From 2c084078c0c4201331f5bb7b28b23f2ca7fdc9d0 Mon Sep 17 00:00:00 2001 From: Kevin Hamilton Date: Thu, 16 May 2024 10:18:40 +0100 Subject: [PATCH 01/18] Initial implementation of placeholders within translated text Signed-off-by: Kevin Hamilton --- src/ConfigProvider.php | 7 +- src/Translator/LoaderPluginManager.php | 10 +-- .../Placeholder/HandlebarPlaceholder.php | 23 ++++++ src/Translator/Placeholder/IcuPlaceholder.php | 22 ++++++ .../Placeholder/PlaceholderInterface.php | 13 +++ .../Placeholder/PrintfPlaceholder.php | 34 ++++++++ .../Placeholder/SegmentPlaceholder.php | 64 +++++++++++++++ src/Translator/PlaceholderPluginManager.php | 70 ++++++++++++++++ .../PlaceholderPluginManagerFactory.php | 79 +++++++++++++++++++ src/Translator/Translator.php | 28 +++++-- src/Translator/TranslatorServiceFactory.php | 59 +++++++++----- .../TranslatorServiceFactoryTest.php | 39 +++++++-- 12 files changed, 407 insertions(+), 41 deletions(-) create mode 100644 src/Translator/Placeholder/HandlebarPlaceholder.php create mode 100644 src/Translator/Placeholder/IcuPlaceholder.php create mode 100644 src/Translator/Placeholder/PlaceholderInterface.php create mode 100644 src/Translator/Placeholder/PrintfPlaceholder.php create mode 100644 src/Translator/Placeholder/SegmentPlaceholder.php create mode 100644 src/Translator/PlaceholderPluginManager.php create mode 100644 src/Translator/PlaceholderPluginManagerFactory.php diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index c42d5d39..1c20f0ef 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -52,9 +52,10 @@ public function getDependencyConfig() Geography\CountryCodeListInterface::class => Geography\DefaultCountryCodeList::class, ], 'factories' => [ - Translator\TranslatorInterface::class => Translator\TranslatorServiceFactory::class, - Translator\LoaderPluginManager::class => Translator\LoaderPluginManagerFactory::class, - Geography\DefaultCountryCodeList::class => [Geography\DefaultCountryCodeList::class, 'create'], + Translator\TranslatorInterface::class => Translator\TranslatorServiceFactory::class, + Translator\LoaderPluginManager::class => Translator\LoaderPluginManagerFactory::class, + Translator\PlaceholderPluginManager::class => Translator\PlaceholderPluginManagerFactory::class, + Geography\DefaultCountryCodeList::class => [Geography\DefaultCountryCodeList::class, 'create'], ], ]; } diff --git a/src/Translator/LoaderPluginManager.php b/src/Translator/LoaderPluginManager.php index f7a15663..f81c8611 100644 --- a/src/Translator/LoaderPluginManager.php +++ b/src/Translator/LoaderPluginManager.php @@ -98,21 +98,21 @@ class LoaderPluginManager extends AbstractPluginManager * Checks that the filter loaded is an instance of * Loader\FileLoaderInterface or Loader\RemoteLoaderInterface. * - * @param mixed $plugin + * @param mixed $instance * @return void * @throws Exception\RuntimeException If invalid. - * @psalm-assert InstanceType $plugin + * @psalm-assert InstanceType $instance */ - public function validate($plugin) + public function validate($instance) { - if ($plugin instanceof FileLoaderInterface || $plugin instanceof RemoteLoaderInterface) { + if ($instance instanceof FileLoaderInterface || $instance instanceof RemoteLoaderInterface) { // we're okay return; } throw new InvalidServiceException(sprintf( 'Plugin of type %s is invalid; must implement %s or %s', - is_object($plugin) ? $plugin::class : gettype($plugin), + is_object($instance) ? $instance::class : gettype($instance), FileLoaderInterface::class, RemoteLoaderInterface::class )); diff --git a/src/Translator/Placeholder/HandlebarPlaceholder.php b/src/Translator/Placeholder/HandlebarPlaceholder.php new file mode 100644 index 00000000..f0e598da --- /dev/null +++ b/src/Translator/Placeholder/HandlebarPlaceholder.php @@ -0,0 +1,23 @@ + $placeholders + */ + public function compile(string $locale, string $message, iterable $placeholders = []): string + { + $compiled = $message; + foreach ($placeholders as $key => $value) { + $compiled = str_replace("{{{$key}}}", $value, $compiled); + } + + return $compiled; + } +} diff --git a/src/Translator/Placeholder/IcuPlaceholder.php b/src/Translator/Placeholder/IcuPlaceholder.php new file mode 100644 index 00000000..d0f6609b --- /dev/null +++ b/src/Translator/Placeholder/IcuPlaceholder.php @@ -0,0 +1,22 @@ + $placeholders + */ + public function compile(string $locale, string $message, iterable $placeholders = []): string; +} diff --git a/src/Translator/Placeholder/PrintfPlaceholder.php b/src/Translator/Placeholder/PrintfPlaceholder.php new file mode 100644 index 00000000..de913297 --- /dev/null +++ b/src/Translator/Placeholder/PrintfPlaceholder.php @@ -0,0 +1,34 @@ + $placeholders + */ + public function compile(string $locale, string $message, iterable $placeholders = []): string + { + if ($placeholders instanceof Traversable) { + $placeholders = iterator_to_array($placeholders); + } + + /** @var string|false $compiled */ + $compiled = call_user_func_array('vsprintf', [$message, $placeholders]); + if ($compiled === false) { + throw new ParseException( + 'Error occurred while processing sprintf placeholders for message "' . $message . '"' + ); + } + + return $compiled; + } +} diff --git a/src/Translator/Placeholder/SegmentPlaceholder.php b/src/Translator/Placeholder/SegmentPlaceholder.php new file mode 100644 index 00000000..cc96cfe8 --- /dev/null +++ b/src/Translator/Placeholder/SegmentPlaceholder.php @@ -0,0 +1,64 @@ + strlen((string) $b); + }); + + $compiled = $message; + foreach ($placeholders as $key => $value) { + $key = (string) $key; + $compiled = str_replace([':' . $key, ':' . strtoupper($key), ':' . ucfirst($key)], [ + $value, + strtoupper($value), + ucfirst($value), + ], $compiled); + } + } catch (Throwable $e) { + throw new ParseException( + 'An error occurred while replacing placeholders in the message', + 0, + $e + ); + } + + return $compiled; + } +} diff --git a/src/Translator/PlaceholderPluginManager.php b/src/Translator/PlaceholderPluginManager.php new file mode 100644 index 00000000..be6a6dd4 --- /dev/null +++ b/src/Translator/PlaceholderPluginManager.php @@ -0,0 +1,70 @@ + + * @method Placeholder\PlaceholderInterface get(string $name) + */ +class PlaceholderPluginManager extends AbstractPluginManager +{ + /** @inheritDoc */ + protected $aliases = [ + 'colon' => Placeholder\SegmentPlaceholder::class, + 'laravel' => Placeholder\SegmentPlaceholder::class, + 'handlebar' => Placeholder\HandlebarPlaceholder::class, + 'handlebars' => Placeholder\HandlebarPlaceholder::class, + 'icu' => Placeholder\IcuPlaceholder::class, + 'vsprintf' => Placeholder\PrintfPlaceholder::class, + 'sprintf' => Placeholder\PrintfPlaceholder::class, + 'printf' => Placeholder\PrintfPlaceholder::class, + ]; + + /** @inheritDoc */ + protected $factories = [ + Placeholder\SegmentPlaceholder::class => InvokableFactory::class, + Placeholder\HandlebarPlaceholder::class => InvokableFactory::class, + Placeholder\IcuPlaceholder::class => InvokableFactory::class, + Placeholder\PrintfPlaceholder::class => InvokableFactory::class, + ]; + + /** + * Validate the plugin. + * + * Checks that the filter loaded is an instance of + * Loader\FileLoaderInterface or Loader\RemoteLoaderInterface. + * + * @throws Exception\RuntimeException If invalid. + * @psalm-assert RemoteLoaderInterface $instance + */ + public function validate(mixed $instance): void + { + if ($instance instanceof Placeholder\PlaceholderInterface) { + // we're okay + return; + } + + throw new InvalidServiceException(sprintf( + 'Plugin of type %s is invalid; must implement %s', + is_object($instance) ? $instance::class : gettype($instance), + Placeholder\PlaceholderInterface::class + )); + } +} diff --git a/src/Translator/PlaceholderPluginManagerFactory.php b/src/Translator/PlaceholderPluginManagerFactory.php new file mode 100644 index 00000000..bb505c96 --- /dev/null +++ b/src/Translator/PlaceholderPluginManagerFactory.php @@ -0,0 +1,79 @@ +|null $options + * @psalm-param ServiceManagerConfiguration|null $options + * @return LoaderPluginManager + */ + public function __invoke(ContainerInterface $container, $name, ?array $options = null) + { + $options = $options ?? []; + $pluginManager = new PlaceholderPluginManager($container, $options); + + // If this is in a laminas-mvc application, the ServiceListener will inject + // merged configuration during bootstrap. + if ($container->has('ServiceListener')) { + return $pluginManager; + } + + // If we do not have a config service, nothing more to do + if (! $container->has('config')) { + return $pluginManager; + } + + $config = $container->get('config'); + + // If we do not have translator_plugins configuration, nothing more to do + if (! isset($config['translator_placeholders']) || ! is_array($config['translator_placeholders'])) { + return $pluginManager; + } + + // Wire service configuration for translator_plugins + (new Config($config['translator_placeholders']))->configureServiceManager($pluginManager); + + return $pluginManager; + } + + /** + * laminas-servicemanager v2 factory to return LoaderPluginManager + * + * @deprecated Since 2.16.0 - This component is no longer compatible with Service Manager v2. + * This method will be removed in version 3.0 + * + * @return LoaderPluginManager + */ + public function createService(ServiceLocatorInterface $container) + { + return $this($container, 'TranslatorPluginManager', $this->creationOptions); + } + + /** + * v2 support for instance creation options. + * + * @deprecated Since 2.16.0 - This component is no longer compatible with Service Manager v2. + * This method will be removed in version 3.0 + * + * @param array $options + * @return void + */ + public function setCreationOptions(array $options) + { + $this->creationOptions = $options; + } +} diff --git a/src/Translator/Translator.php b/src/Translator/Translator.php index 597d2395..c54cc7fb 100644 --- a/src/Translator/Translator.php +++ b/src/Translator/Translator.php @@ -10,6 +10,7 @@ use Laminas\I18n\Exception; use Laminas\I18n\Translator\Loader\FileLoaderInterface; use Laminas\I18n\Translator\Loader\RemoteLoaderInterface; +use Laminas\I18n\Translator\Placeholder\PlaceholderInterface; use Laminas\ServiceManager\ServiceManager; use Laminas\Stdlib\ArrayUtils; use Locale; @@ -109,6 +110,8 @@ class Translator implements TranslatorInterface */ protected $eventsEnabled = false; + protected ?PlaceholderInterface $placeholder = null; + /** * Instantiate a translator * @@ -338,14 +341,14 @@ public function getPluginManager() /** * Translate a message. * - * @param string $message - * @param string $textDomain - * @param string|null $locale + * @param string $message + * @param string|string[] $textDomain + * @param string|null $locale * @return string */ public function translate($message, $textDomain = 'default', $locale = null) { - $locale = $locale ?? $this->getLocale(); + $locale ??= $this->getLocale(); $translation = $this->getTranslatedMessage($message, $locale, $textDomain); if ($translation !== null && $translation !== '') { @@ -418,9 +421,9 @@ public function translatePlural( * Get a translated message. * * @triggers getTranslatedMessage.missing-translation - * @param string $message - * @param string $locale - * @param string $textDomain + * @param string $message + * @param string $locale + * @param string|string[] $textDomain or placeholders * @return string|null */ protected function getTranslatedMessage( @@ -432,6 +435,12 @@ protected function getTranslatedMessage( return ''; } + $placeholders = []; + if (is_array($textDomain)) { + $placeholders = $textDomain; + $textDomain = $placeholders['_textDomain'] ?? 'default'; + } + if (! isset($this->messages[$textDomain][$locale])) { $this->loadMessages($textDomain, $locale); } @@ -823,4 +832,9 @@ public function disableEventManager() $this->eventsEnabled = false; return $this; } + + public function setPlaceholder(PlaceholderInterface $placeholder) + { + $this->placeholder = $placeholder; + } } diff --git a/src/Translator/TranslatorServiceFactory.php b/src/Translator/TranslatorServiceFactory.php index d7b61f8a..3830bb1e 100644 --- a/src/Translator/TranslatorServiceFactory.php +++ b/src/Translator/TranslatorServiceFactory.php @@ -2,9 +2,17 @@ namespace Laminas\I18n\Translator; -use Laminas\ServiceManager\FactoryInterface; -use Laminas\ServiceManager\ServiceLocatorInterface; +use Laminas\ServiceManager\Exception\InvalidServiceException; +use Laminas\ServiceManager\Exception\ServiceNotCreatedException; +use Laminas\ServiceManager\Factory\FactoryInterface; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; + +use function gettype; +use function is_object; +use function is_string; +use function sprintf; /** * Translator. @@ -15,33 +23,44 @@ class TranslatorServiceFactory implements FactoryInterface * Create a Translator instance. * * @param string $requestedName - * @param null|array $options - * @return Translator + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ - public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null) + public function __invoke(ContainerInterface $container, $requestedName, ?array $options = null): Translator { // Configure the translator + /** @var array $config */ $config = $container->get('config'); $trConfig = $config['translator'] ?? []; $translator = Translator::factory($trConfig); if ($container->has('TranslatorPluginManager')) { $translator->setPluginManager($container->get('TranslatorPluginManager')); } - return $translator; - } - /** - * laminas-servicemanager v2 factory for creating Translator instance. - * - * @deprecated Since 2.16.0 - This component is no longer compatible with Service Manager v2. - * This method will be removed in version 3.0 - * - * Proxies to `__invoke()`. - * - * @return Translator - */ - public function createService(ServiceLocatorInterface $serviceLocator) - { - return $this($serviceLocator, Translator::class); + /** @var PlaceholderPluginManager $placeholderManager */ + $placeholderManager = $container->get(PlaceholderPluginManager::class); + /** @var mixed $placeholderName */ + $placeholderName = $trConfig['placeholder_format'] ?? 'handlebars'; + if ($placeholderName instanceof Placeholder\PlaceholderInterface) { + $placeholder = $placeholderName; + } elseif (is_string($placeholderName)) { + if (! $placeholderManager->has($placeholderName)) { + throw new ServiceNotCreatedException( + sprintf('Could not find a placeholder format with the name "%s"', $placeholderName) + ); + } + + $placeholder = $placeholderManager->get($placeholderName); + } else { + throw new InvalidServiceException(sprintf( + '\'placeholder_format\' of type %s is invalid; must be a string or object that implements %s', + is_object($placeholderName) ? $placeholderName::class : gettype($placeholderName), + Placeholder\PlaceholderInterface::class + )); + } + + $translator->setPlaceholder($placeholder); + + return $translator; } } diff --git a/test/Translator/TranslatorServiceFactoryTest.php b/test/Translator/TranslatorServiceFactoryTest.php index b9ea4df3..dd165953 100644 --- a/test/Translator/TranslatorServiceFactoryTest.php +++ b/test/Translator/TranslatorServiceFactoryTest.php @@ -5,6 +5,8 @@ namespace LaminasTest\I18n\Translator; use Laminas\I18n\Translator\LoaderPluginManager; +use Laminas\I18n\Translator\Placeholder\HandlebarPlaceholder; +use Laminas\I18n\Translator\PlaceholderPluginManager; use Laminas\I18n\Translator\Translator; use Laminas\I18n\Translator\TranslatorServiceFactory; use LaminasTest\I18n\TestCase; @@ -14,7 +16,8 @@ class TranslatorServiceFactoryTest extends TestCase { public function testCreateServiceWithNoTranslatorKeyDefined(): void { - $pluginManagerMock = $this->createMock(LoaderPluginManager::class); + $pluginManagerMock = $this->createMock(LoaderPluginManager::class); + $placeholderManagerMock = $this->createMock(PlaceholderPluginManager::class); $serviceLocator = $this->createMock(ContainerInterface::class); $serviceLocator->expects(self::once()) @@ -22,10 +25,21 @@ public function testCreateServiceWithNoTranslatorKeyDefined(): void ->with('TranslatorPluginManager') ->willReturn(true); - $serviceLocator->expects(self::exactly(2)) + $placeholderManagerMock->expects(self::once()) + ->method('has') + ->with('handlebars') + ->willReturn(true); + + $placeholderManagerMock->expects(self::once()) + ->method('get') + ->with('handlebars') + ->willReturn(new HandlebarPlaceholder()); + + $serviceLocator->expects(self::exactly(3)) ->method('get') ->willReturnMap([ ['TranslatorPluginManager', $pluginManagerMock], + [PlaceholderPluginManager::class, $placeholderManagerMock], ['config', []], ]); @@ -43,10 +57,23 @@ public function testCreateServiceWithNoTranslatorPluginManagerDefined(): void ->with('TranslatorPluginManager') ->willReturn(false); - $serviceLocator->expects(self::once()) - ->method('get') - ->with('config') - ->willReturn([]); + $placeholderManagerMock = $this->createMock(PlaceholderPluginManager::class); + $serviceLocator->expects(self::exactly(2)) + ->method('get') + ->willReturnMap([ + [PlaceholderPluginManager::class, $placeholderManagerMock], + ['config', []], + ]); + + $placeholderManagerMock->expects(self::once()) + ->method('has') + ->with('handlebars') + ->willReturn(true); + + $placeholderManagerMock->expects(self::once()) + ->method('get') + ->with('handlebars') + ->willReturn(new HandlebarPlaceholder()); $factory = new TranslatorServiceFactory(); $translator = $factory($serviceLocator, Translator::class); From 7268f055d4484ba0134feea34ca47c58f247c037 Mon Sep 17 00:00:00 2001 From: Kevin Hamilton Date: Thu, 16 May 2024 13:23:25 +0100 Subject: [PATCH 02/18] #7: Pass translated message through placeholder compile Signed-off-by: Kevin Hamilton --- src/Translator/PlaceholderPluginManager.php | 3 ++- src/Translator/Translator.php | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Translator/PlaceholderPluginManager.php b/src/Translator/PlaceholderPluginManager.php index be6a6dd4..802bb372 100644 --- a/src/Translator/PlaceholderPluginManager.php +++ b/src/Translator/PlaceholderPluginManager.php @@ -21,12 +21,13 @@ * * @template InstanceType of Placeholder\PlaceholderInterface * @extends AbstractPluginManager - * @method Placeholder\PlaceholderInterface get(string $name) + * @method Placeholder\PlaceholderInterface get(string $name, ?array $options = null) */ class PlaceholderPluginManager extends AbstractPluginManager { /** @inheritDoc */ protected $aliases = [ + 'segment' => Placeholder\SegmentPlaceholder::class, 'colon' => Placeholder\SegmentPlaceholder::class, 'laravel' => Placeholder\SegmentPlaceholder::class, 'handlebar' => Placeholder\HandlebarPlaceholder::class, diff --git a/src/Translator/Translator.php b/src/Translator/Translator.php index c54cc7fb..2923821e 100644 --- a/src/Translator/Translator.php +++ b/src/Translator/Translator.php @@ -446,7 +446,13 @@ protected function getTranslatedMessage( } if (isset($this->messages[$textDomain][$locale][$message])) { - return $this->messages[$textDomain][$locale][$message]; + return $this->placeholder ? + $this->placeholder->compile( + $locale, + $this->messages[$textDomain][$locale][$message], + $placeholders + ) : + $this->messages[$textDomain][$locale][$message]; } /** @@ -463,7 +469,13 @@ protected function getTranslatedMessage( * ] */ if (isset($this->messages[$textDomain][$locale][$textDomain . "\x04" . $message])) { - return $this->messages[$textDomain][$locale][$textDomain . "\x04" . $message]; + return $this->placeholder ? + $this->placeholder->compile( + $locale, + $this->messages[$textDomain][$locale][$textDomain . "\x04" . $message], + $placeholders + ) : + $this->messages[$textDomain][$locale][$textDomain . "\x04" . $message]; } if ($this->isEventManagerEnabled()) { From 3f9c7c9d343f75e5b40719bfe4834c82521bb9aa Mon Sep 17 00:00:00 2001 From: Kevin Hamilton Date: Thu, 16 May 2024 14:21:59 +0100 Subject: [PATCH 03/18] #7: Ensure text without a translation is still compiled Signed-off-by: Kevin Hamilton --- src/Translator/Translator.php | 37 +++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/Translator/Translator.php b/src/Translator/Translator.php index 2923821e..8d14cc6e 100644 --- a/src/Translator/Translator.php +++ b/src/Translator/Translator.php @@ -362,7 +362,7 @@ public function translate($message, $textDomain = 'default', $locale = null) return $this->translate($message, $textDomain, $fallbackLocale); } - return $message; + return $this->compileMessage($message, is_array($textDomain) ? $textDomain : []); } /** @@ -446,13 +446,11 @@ protected function getTranslatedMessage( } if (isset($this->messages[$textDomain][$locale][$message])) { - return $this->placeholder ? - $this->placeholder->compile( - $locale, - $this->messages[$textDomain][$locale][$message], - $placeholders - ) : - $this->messages[$textDomain][$locale][$message]; + return $this->compileMessage( + $this->messages[$textDomain][$locale][$message], + $placeholders, + $locale + ); } /** @@ -469,13 +467,11 @@ protected function getTranslatedMessage( * ] */ if (isset($this->messages[$textDomain][$locale][$textDomain . "\x04" . $message])) { - return $this->placeholder ? - $this->placeholder->compile( - $locale, - $this->messages[$textDomain][$locale][$textDomain . "\x04" . $message], - $placeholders - ) : - $this->messages[$textDomain][$locale][$textDomain . "\x04" . $message]; + return $this->compileMessage( + $this->messages[$textDomain][$locale][$textDomain . "\x04" . $message], + $placeholders, + $locale + ); } if ($this->isEventManagerEnabled()) { @@ -849,4 +845,15 @@ public function setPlaceholder(PlaceholderInterface $placeholder) { $this->placeholder = $placeholder; } + + protected function compileMessage(string $message, array $placeholders, string $locale): string + { + return $this->placeholder ? + $this->placeholder->compile( + $locale, + $message, + $placeholders + ) : + $message; + } } From feac4412f02ce979c53ba095b47c88b1f4f08459 Mon Sep 17 00:00:00 2001 From: Kevin Hamilton Date: Thu, 16 May 2024 15:27:15 +0100 Subject: [PATCH 04/18] #7: Move compilation to translate methods Signed-off-by: Kevin Hamilton --- src/Translator/Translator.php | 62 +++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/src/Translator/Translator.php b/src/Translator/Translator.php index 8d14cc6e..910983b2 100644 --- a/src/Translator/Translator.php +++ b/src/Translator/Translator.php @@ -348,7 +348,13 @@ public function getPluginManager() */ public function translate($message, $textDomain = 'default', $locale = null) { - $locale ??= $this->getLocale(); + $locale ??= $this->getLocale(); + $placeholders = []; + if (is_array($textDomain)) { + $placeholders = $textDomain; + $textDomain = $placeholders['_textDomain'] ?? 'default'; + } + $translation = $this->getTranslatedMessage($message, $locale, $textDomain); if ($translation !== null && $translation !== '') { @@ -362,7 +368,7 @@ public function translate($message, $textDomain = 'default', $locale = null) return $this->translate($message, $textDomain, $fallbackLocale); } - return $this->compileMessage($message, is_array($textDomain) ? $textDomain : []); + return $this->compileMessage($message, $placeholders, $locale); } /** @@ -383,7 +389,12 @@ public function translatePlural( $textDomain = 'default', $locale = null ) { - $locale = $locale ?? $this->getLocale(); + $locale ??= $this->getLocale(); + $placeholders = []; + if (is_array($textDomain)) { + $placeholders = $textDomain; + $textDomain = $placeholders['_textDomain'] ?? 'default'; + } $translation = $this->getTranslatedMessage($singular, $locale, $textDomain); if (is_string($translation)) { @@ -398,23 +409,27 @@ public function translatePlural( } if (isset($translation[$index]) && $translation[$index] !== '' && $translation[$index] !== null) { - return $translation[$index]; + return $this->compileMessage($translation[$index], $placeholders, $locale); } if ( null !== ($fallbackLocale = $this->getFallbackLocale()) && $locale !== $fallbackLocale ) { - return $this->translatePlural( - $singular, - $plural, - $number, - $textDomain, - $fallbackLocale + return $this->compileMessage( + $this->translatePlural( + $singular, + $plural, + $number, + $textDomain, + $fallbackLocale + ), + $placeholders, + $locale ); } - return $index === 0 ? $singular : $plural; + return $this->compileMessage($index === 0 ? $singular : $plural, $placeholders, $locale); } /** @@ -424,7 +439,7 @@ public function translatePlural( * @param string $message * @param string $locale * @param string|string[] $textDomain or placeholders - * @return string|null + * @return string|array|null */ protected function getTranslatedMessage( $message, @@ -435,22 +450,12 @@ protected function getTranslatedMessage( return ''; } - $placeholders = []; - if (is_array($textDomain)) { - $placeholders = $textDomain; - $textDomain = $placeholders['_textDomain'] ?? 'default'; - } - if (! isset($this->messages[$textDomain][$locale])) { $this->loadMessages($textDomain, $locale); } if (isset($this->messages[$textDomain][$locale][$message])) { - return $this->compileMessage( - $this->messages[$textDomain][$locale][$message], - $placeholders, - $locale - ); + return $this->messages[$textDomain][$locale][$message]; } /** @@ -467,11 +472,7 @@ protected function getTranslatedMessage( * ] */ if (isset($this->messages[$textDomain][$locale][$textDomain . "\x04" . $message])) { - return $this->compileMessage( - $this->messages[$textDomain][$locale][$textDomain . "\x04" . $message], - $placeholders, - $locale - ); + return $this->messages[$textDomain][$locale][$textDomain . "\x04" . $message]; } if ($this->isEventManagerEnabled()) { @@ -846,7 +847,10 @@ public function setPlaceholder(PlaceholderInterface $placeholder) $this->placeholder = $placeholder; } - protected function compileMessage(string $message, array $placeholders, string $locale): string + /** + * @param iterable $placeholders + */ + protected function compileMessage(string $message, iterable $placeholders, string $locale): string { return $this->placeholder ? $this->placeholder->compile( From 7d257af8fb878a114b1a5bad1ac21b6269b6090e Mon Sep 17 00:00:00 2001 From: Kevin Hamilton Date: Thu, 16 May 2024 15:30:22 +0100 Subject: [PATCH 05/18] #7: Changes to pass tests Signed-off-by: Kevin Hamilton --- src/Translator/Translator.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Translator/Translator.php b/src/Translator/Translator.php index 910983b2..5c31bd2c 100644 --- a/src/Translator/Translator.php +++ b/src/Translator/Translator.php @@ -850,9 +850,9 @@ public function setPlaceholder(PlaceholderInterface $placeholder) /** * @param iterable $placeholders */ - protected function compileMessage(string $message, iterable $placeholders, string $locale): string + protected function compileMessage(?string $message, iterable $placeholders, string $locale): ?string { - return $this->placeholder ? + return $this->placeholder && $message ? $this->placeholder->compile( $locale, $message, From f07eef4003a34891fabd8672239d6317182975c6 Mon Sep 17 00:00:00 2001 From: Kevin Hamilton Date: Thu, 16 May 2024 15:50:56 +0100 Subject: [PATCH 06/18] #7: Missed returns Signed-off-by: Kevin Hamilton --- src/Translator/Translator.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Translator/Translator.php b/src/Translator/Translator.php index 5c31bd2c..85ca0f22 100644 --- a/src/Translator/Translator.php +++ b/src/Translator/Translator.php @@ -358,14 +358,14 @@ public function translate($message, $textDomain = 'default', $locale = null) $translation = $this->getTranslatedMessage($message, $locale, $textDomain); if ($translation !== null && $translation !== '') { - return $translation; + return $this->compileMessage($translation, $placeholders, $locale); } if ( null !== ($fallbackLocale = $this->getFallbackLocale()) && $locale !== $fallbackLocale ) { - return $this->translate($message, $textDomain, $fallbackLocale); + return $this->translate($message, $placeholders ?: $textDomain, $fallbackLocale); } return $this->compileMessage($message, $placeholders, $locale); From feee2606e90e099cfc708930d9cbbbb293c9fc9c Mon Sep 17 00:00:00 2001 From: Kevin Hamilton Date: Thu, 16 May 2024 17:30:58 +0100 Subject: [PATCH 07/18] Better type inference for psalm Signed-off-by: Kevin Hamilton --- .../Placeholder/HandlebarPlaceholder.php | 3 - .../Placeholder/PrintfPlaceholder.php | 3 - .../PlaceholderPluginManagerFactory.php | 46 +++------- src/Translator/Translator.php | 85 ++++++++++--------- 4 files changed, 59 insertions(+), 78 deletions(-) diff --git a/src/Translator/Placeholder/HandlebarPlaceholder.php b/src/Translator/Placeholder/HandlebarPlaceholder.php index f0e598da..bbcac635 100644 --- a/src/Translator/Placeholder/HandlebarPlaceholder.php +++ b/src/Translator/Placeholder/HandlebarPlaceholder.php @@ -8,9 +8,6 @@ class HandlebarPlaceholder implements PlaceholderInterface { - /** - * @param iterable $placeholders - */ public function compile(string $locale, string $message, iterable $placeholders = []): string { $compiled = $message; diff --git a/src/Translator/Placeholder/PrintfPlaceholder.php b/src/Translator/Placeholder/PrintfPlaceholder.php index de913297..7e32c64a 100644 --- a/src/Translator/Placeholder/PrintfPlaceholder.php +++ b/src/Translator/Placeholder/PrintfPlaceholder.php @@ -12,9 +12,6 @@ class PrintfPlaceholder implements PlaceholderInterface { - /** - * @param iterable $placeholders - */ public function compile(string $locale, string $message, iterable $placeholders = []): string { if ($placeholders instanceof Traversable) { diff --git a/src/Translator/PlaceholderPluginManagerFactory.php b/src/Translator/PlaceholderPluginManagerFactory.php index bb505c96..b2ef2296 100644 --- a/src/Translator/PlaceholderPluginManagerFactory.php +++ b/src/Translator/PlaceholderPluginManagerFactory.php @@ -6,7 +6,9 @@ use Laminas\ServiceManager\Factory\FactoryInterface; use Laminas\ServiceManager\ServiceLocatorInterface; use Laminas\ServiceManager\ServiceManager; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; use function is_array; @@ -16,14 +18,17 @@ class PlaceholderPluginManagerFactory implements FactoryInterface /** * Create and return a PlaceholderPluginManager. * - * @param string $name - * @param array|null $options - * @psalm-param ServiceManagerConfiguration|null $options - * @return LoaderPluginManager + * @param string $requestedName + * @param array|null $options + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ - public function __invoke(ContainerInterface $container, $name, ?array $options = null) - { - $options = $options ?? []; + public function __invoke( + ContainerInterface $container, + $requestedName, + ?array $options = null + ): PlaceholderPluginManager { + $options ??= []; $pluginManager = new PlaceholderPluginManager($container, $options); // If this is in a laminas-mvc application, the ServiceListener will inject @@ -49,31 +54,4 @@ public function __invoke(ContainerInterface $container, $name, ?array $options = return $pluginManager; } - - /** - * laminas-servicemanager v2 factory to return LoaderPluginManager - * - * @deprecated Since 2.16.0 - This component is no longer compatible with Service Manager v2. - * This method will be removed in version 3.0 - * - * @return LoaderPluginManager - */ - public function createService(ServiceLocatorInterface $container) - { - return $this($container, 'TranslatorPluginManager', $this->creationOptions); - } - - /** - * v2 support for instance creation options. - * - * @deprecated Since 2.16.0 - This component is no longer compatible with Service Manager v2. - * This method will be removed in version 3.0 - * - * @param array $options - * @return void - */ - public function setCreationOptions(array $options) - { - $this->creationOptions = $options; - } } diff --git a/src/Translator/Translator.php b/src/Translator/Translator.php index 85ca0f22..9ff4e0b4 100644 --- a/src/Translator/Translator.php +++ b/src/Translator/Translator.php @@ -3,6 +3,7 @@ namespace Laminas\I18n\Translator; use Laminas\Cache; +use Laminas\Cache\Exception\ExceptionInterface; use Laminas\Cache\Storage\StorageInterface as CacheStorage; use Laminas\EventManager\Event; use Laminas\EventManager\EventManager; @@ -43,28 +44,28 @@ class Translator implements TranslatorInterface /** * Messages loaded by the translator. * - * @var array + * @var array|TextDomain> */ protected $messages = []; /** * Files used for loading messages. * - * @var array + * @var array>> */ protected $files = []; /** * Patterns used for loading messages. * - * @var array + * @var array>> */ protected $patterns = []; /** * Remote locations for loading messages. * - * @var array + * @var array> */ protected $remote = []; @@ -357,7 +358,7 @@ public function translate($message, $textDomain = 'default', $locale = null) $translation = $this->getTranslatedMessage($message, $locale, $textDomain); - if ($translation !== null && $translation !== '') { + if (is_string($translation) && $translation !== '') { return $this->compileMessage($translation, $placeholders, $locale); } @@ -374,11 +375,11 @@ public function translate($message, $textDomain = 'default', $locale = null) /** * Translate a plural message. * - * @param string $singular - * @param string $plural - * @param int $number - * @param string $textDomain - * @param string|null $locale + * @param string $singular + * @param string $plural + * @param int $number + * @param string|string[] $textDomain + * @param string|null $locale * @return string * @throws Exception\OutOfBoundsException */ @@ -408,7 +409,7 @@ public function translatePlural( ->evaluate($number); } - if (isset($translation[$index]) && $translation[$index] !== '' && $translation[$index] !== null) { + if (isset($translation[$index]) && is_string($translation[$index]) && $translation[$index] !== '') { return $this->compileMessage($translation[$index], $placeholders, $locale); } @@ -436,9 +437,9 @@ public function translatePlural( * Get a translated message. * * @triggers getTranslatedMessage.missing-translation - * @param string $message - * @param string $locale - * @param string|string[] $textDomain or placeholders + * @param string $message + * @param string $locale + * @param string $textDomain * @return string|array|null */ protected function getTranslatedMessage( @@ -510,12 +511,16 @@ public function addTranslationFile( $textDomain = 'default', $locale = null ) { - $locale = $locale ?? '*'; + $locale ??= '*'; if (! isset($this->files[$textDomain])) { $this->files[$textDomain] = []; } + if (! isset($this->files[$textDomain][$locale])) { + $this->files[$textDomain][$locale] = []; + } + $this->files[$textDomain][$locale][] = [ 'type' => $type, 'filename' => $filename, @@ -585,9 +590,10 @@ public function getCacheId($textDomain, $locale) /** * Clears the cache for a specific textDomain and locale. * - * @param string $textDomain - * @param string $locale + * @param string $textDomain + * @param string $locale * @return bool + * @throws ExceptionInterface */ public function clearCache($textDomain, $locale) { @@ -612,6 +618,7 @@ protected function loadMessages($textDomain, $locale) $this->messages[$textDomain] = []; } + $cacheId = ''; if (null !== ($cache = $this->getCache())) { $cacheId = $this->getCacheId($textDomain, $locale); @@ -673,12 +680,7 @@ protected function loadMessagesFromRemote($textDomain, $locale) throw new Exception\RuntimeException('Specified loader is not a remote loader'); } - if (isset($this->messages[$textDomain][$locale])) { - $this->messages[$textDomain][$locale]->merge($loader->load($locale, $textDomain)); - } else { - $this->messages[$textDomain][$locale] = $loader->load($locale, $textDomain); - } - + $this->storeTextDomain($textDomain, $locale, $loader->load($locale, $textDomain)); $messagesLoaded = true; } } @@ -709,11 +711,7 @@ protected function loadMessagesFromPatterns($textDomain, $locale) throw new Exception\RuntimeException('Specified loader is not a file loader'); } - if (isset($this->messages[$textDomain][$locale])) { - $this->messages[$textDomain][$locale]->merge($loader->load($locale, $filename)); - } else { - $this->messages[$textDomain][$locale] = $loader->load($locale, $filename); - } + $this->storeTextDomain($textDomain, $locale, $loader->load($locale, $filename)); $messagesLoaded = true; } @@ -723,6 +721,22 @@ protected function loadMessagesFromPatterns($textDomain, $locale) return $messagesLoaded; } + private function storeTextDomain(string $textDomain, string $locale, ?TextDomain $loaded): void + { + if (! $loaded instanceof TextDomain) { + return; + } + + if ( + isset($this->messages[$textDomain][$locale]) && + $this->messages[$textDomain][$locale] instanceof TextDomain + ) { + $this->messages[$textDomain][$locale]->merge($loaded); + } else { + $this->messages[$textDomain][$locale] = $loaded; + } + } + /** * Load messages from files. * @@ -747,12 +761,7 @@ protected function loadMessagesFromFiles($textDomain, $locale) throw new Exception\RuntimeException('Specified loader is not a file loader'); } - if (isset($this->messages[$textDomain][$locale])) { - $this->messages[$textDomain][$locale]->merge($loader->load($locale, $file['filename'])); - } else { - $this->messages[$textDomain][$locale] = $loader->load($locale, $file['filename']); - } - + $this->storeTextDomain($textDomain, $locale, $loader->load($locale, $file['filename'])); $messagesLoaded = true; } @@ -842,7 +851,7 @@ public function disableEventManager() return $this; } - public function setPlaceholder(PlaceholderInterface $placeholder) + public function setPlaceholder(PlaceholderInterface $placeholder): void { $this->placeholder = $placeholder; } @@ -850,14 +859,14 @@ public function setPlaceholder(PlaceholderInterface $placeholder) /** * @param iterable $placeholders */ - protected function compileMessage(?string $message, iterable $placeholders, string $locale): ?string + protected function compileMessage(?string $message, iterable $placeholders, string $locale): string { - return $this->placeholder && $message ? + return $this->placeholder && $message !== '' && $message !== null ? $this->placeholder->compile( $locale, $message, $placeholders ) : - $message; + ($message ?? ''); } } From 1306a097db411c9febffabdfca88e11a390b04fd Mon Sep 17 00:00:00 2001 From: Kevin Hamilton Date: Thu, 16 May 2024 17:37:06 +0100 Subject: [PATCH 08/18] Optimise use Signed-off-by: Kevin Hamilton --- src/Translator/PlaceholderPluginManagerFactory.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Translator/PlaceholderPluginManagerFactory.php b/src/Translator/PlaceholderPluginManagerFactory.php index b2ef2296..61c905ed 100644 --- a/src/Translator/PlaceholderPluginManagerFactory.php +++ b/src/Translator/PlaceholderPluginManagerFactory.php @@ -4,7 +4,6 @@ use Laminas\ServiceManager\Config; use Laminas\ServiceManager\Factory\FactoryInterface; -use Laminas\ServiceManager\ServiceLocatorInterface; use Laminas\ServiceManager\ServiceManager; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; From 1c50b0355d6056cc90ca9bb681a442e503e1f5a2 Mon Sep 17 00:00:00 2001 From: Kevin Hamilton Date: Fri, 17 May 2024 17:26:30 +0100 Subject: [PATCH 09/18] Move translator with params to new methods, add corresponding view helpers Signed-off-by: Kevin Hamilton --- src/ConfigProvider.php | 52 +++---- src/Translator/Translator.php | 128 +++++++++++------- .../TranslatorWithParamsInterface.php | 31 +++++ src/View/Helper/TranslatePluralWithParams.php | 40 ++++++ src/View/Helper/TranslateWithParams.php | 39 ++++++ 5 files changed, 218 insertions(+), 72 deletions(-) create mode 100644 src/Translator/TranslatorWithParamsInterface.php create mode 100644 src/View/Helper/TranslatePluralWithParams.php create mode 100644 src/View/Helper/TranslateWithParams.php diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 1c20f0ef..bebeb8d9 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -160,23 +160,25 @@ public function getViewHelperConfig() { return [ 'aliases' => [ - 'countryCodeDataList' => View\Helper\CountryCodeDataList::class, - 'currencyformat' => View\Helper\CurrencyFormat::class, - 'currencyFormat' => View\Helper\CurrencyFormat::class, - 'CurrencyFormat' => View\Helper\CurrencyFormat::class, - 'dateformat' => View\Helper\DateFormat::class, - 'dateFormat' => View\Helper\DateFormat::class, - 'DateFormat' => View\Helper\DateFormat::class, - 'numberformat' => View\Helper\NumberFormat::class, - 'numberFormat' => View\Helper\NumberFormat::class, - 'NumberFormat' => View\Helper\NumberFormat::class, - 'plural' => View\Helper\Plural::class, - 'Plural' => View\Helper\Plural::class, - 'translate' => View\Helper\Translate::class, - 'Translate' => View\Helper\Translate::class, - 'translateplural' => View\Helper\TranslatePlural::class, - 'translatePlural' => View\Helper\TranslatePlural::class, - 'TranslatePlural' => View\Helper\TranslatePlural::class, + 'countryCodeDataList' => View\Helper\CountryCodeDataList::class, + 'currencyformat' => View\Helper\CurrencyFormat::class, + 'currencyFormat' => View\Helper\CurrencyFormat::class, + 'CurrencyFormat' => View\Helper\CurrencyFormat::class, + 'dateformat' => View\Helper\DateFormat::class, + 'dateFormat' => View\Helper\DateFormat::class, + 'DateFormat' => View\Helper\DateFormat::class, + 'numberformat' => View\Helper\NumberFormat::class, + 'numberFormat' => View\Helper\NumberFormat::class, + 'NumberFormat' => View\Helper\NumberFormat::class, + 'plural' => View\Helper\Plural::class, + 'Plural' => View\Helper\Plural::class, + 'translate' => View\Helper\Translate::class, + 'Translate' => View\Helper\Translate::class, + 'translateplural' => View\Helper\TranslatePlural::class, + 'translatePlural' => View\Helper\TranslatePlural::class, + 'TranslatePlural' => View\Helper\TranslatePlural::class, + 'translateWithParams' => View\Helper\TranslateWithParams::class, + 'translatePluralWithParams' => View\Helper\TranslatePluralWithParams::class, // Legacy Zend Framework aliases 'Zend\I18n\View\Helper\CurrencyFormat' => View\Helper\CurrencyFormat::class, @@ -187,13 +189,15 @@ public function getViewHelperConfig() 'Zend\I18n\View\Helper\TranslatePlural' => View\Helper\TranslatePlural::class, ], 'factories' => [ - View\Helper\CountryCodeDataList::class => View\Helper\Container\CountryCodeDataListFactory::class, - View\Helper\CurrencyFormat::class => InvokableFactory::class, - View\Helper\DateFormat::class => InvokableFactory::class, - View\Helper\NumberFormat::class => InvokableFactory::class, - View\Helper\Plural::class => InvokableFactory::class, - View\Helper\Translate::class => InvokableFactory::class, - View\Helper\TranslatePlural::class => InvokableFactory::class, + View\Helper\CountryCodeDataList::class => View\Helper\Container\CountryCodeDataListFactory::class, + View\Helper\CurrencyFormat::class => InvokableFactory::class, + View\Helper\DateFormat::class => InvokableFactory::class, + View\Helper\NumberFormat::class => InvokableFactory::class, + View\Helper\Plural::class => InvokableFactory::class, + View\Helper\Translate::class => InvokableFactory::class, + View\Helper\TranslatePlural::class => InvokableFactory::class, + View\Helper\TranslateWithParams::class => InvokableFactory::class, + View\Helper\TranslatePluralWithParams::class => InvokableFactory::class, ], ]; } diff --git a/src/Translator/Translator.php b/src/Translator/Translator.php index 9ff4e0b4..004df10d 100644 --- a/src/Translator/Translator.php +++ b/src/Translator/Translator.php @@ -18,6 +18,7 @@ use Traversable; use function array_shift; +use function ctype_digit; use function get_debug_type; use function is_array; use function is_file; @@ -29,7 +30,7 @@ /** * Translator. */ -class Translator implements TranslatorInterface +class Translator implements TranslatorInterface, TranslatorWithParamsInterface { /** * Event fired when the translation for a message is missing. @@ -44,7 +45,7 @@ class Translator implements TranslatorInterface /** * Messages loaded by the translator. * - * @var array|TextDomain> + * @var array|TextDomain> */ protected $messages = []; @@ -349,27 +350,21 @@ public function getPluginManager() */ public function translate($message, $textDomain = 'default', $locale = null) { - $locale ??= $this->getLocale(); - $placeholders = []; - if (is_array($textDomain)) { - $placeholders = $textDomain; - $textDomain = $placeholders['_textDomain'] ?? 'default'; - } - + $locale ??= $this->getLocale(); $translation = $this->getTranslatedMessage($message, $locale, $textDomain); if (is_string($translation) && $translation !== '') { - return $this->compileMessage($translation, $placeholders, $locale); + return $translation; } if ( null !== ($fallbackLocale = $this->getFallbackLocale()) && $locale !== $fallbackLocale ) { - return $this->translate($message, $placeholders ?: $textDomain, $fallbackLocale); + return $this->translate($message, $textDomain, $fallbackLocale); } - return $this->compileMessage($message, $placeholders, $locale); + return $message; } /** @@ -390,12 +385,7 @@ public function translatePlural( $textDomain = 'default', $locale = null ) { - $locale ??= $this->getLocale(); - $placeholders = []; - if (is_array($textDomain)) { - $placeholders = $textDomain; - $textDomain = $placeholders['_textDomain'] ?? 'default'; - } + $locale ??= $this->getLocale(); $translation = $this->getTranslatedMessage($singular, $locale, $textDomain); if (is_string($translation)) { @@ -403,34 +393,76 @@ public function translatePlural( } $index = $number === 1 ? 0 : 1; // en_EN Plural rule - if ($this->messages[$textDomain][$locale] instanceof TextDomain) { + if ( + isset($this->messages[$textDomain][$locale]) && + $this->messages[$textDomain][$locale] instanceof TextDomain + ) { $index = $this->messages[$textDomain][$locale] ->getPluralRule() ->evaluate($number); } if (isset($translation[$index]) && is_string($translation[$index]) && $translation[$index] !== '') { - return $this->compileMessage($translation[$index], $placeholders, $locale); + return $translation[$index]; } if ( null !== ($fallbackLocale = $this->getFallbackLocale()) && $locale !== $fallbackLocale ) { - return $this->compileMessage( - $this->translatePlural( - $singular, - $plural, - $number, - $textDomain, - $fallbackLocale - ), - $placeholders, - $locale + return $this->translatePlural( + $singular, + $plural, + $number, + $textDomain, + $fallbackLocale ); } - return $this->compileMessage($index === 0 ? $singular : $plural, $placeholders, $locale); + return $index === 0 ? $singular : $plural; + } + + /** + * @param iterable $params + */ + public function translateWithParams( + string $message, + iterable $params = [], + string $textDomain = 'default', + ?string $locale = null + ): string { + $locale ??= $this->getLocale(); + + return $this->compileMessage($this->translate($message, $textDomain, $locale), $params, $locale); + } + + /** + * The first number in params is used to determine the plural form. + * + * @param iterable $params + */ + public function translatePluralWithParams( + string $singular, + string $plural, + iterable $params = [], + string $textDomain = 'default', + ?string $locale = null + ): string { + $locale ??= $this->getLocale(); + + $number = 1; + foreach ($params as $param) { + if (ctype_digit($param)) { + $number = (int) $param; + break; + } + } + + return $this->compileMessage( + $this->translatePlural($singular, $plural, $number, $textDomain, $locale), + $params, + $locale + ); } /** @@ -477,7 +509,7 @@ protected function getTranslatedMessage( } if ($this->isEventManagerEnabled()) { - $until = static fn($r): bool => is_string($r); + $until = static fn(mixed $r): bool => is_string($r); $event = new Event(self::EVENT_MISSING_TRANSLATION, $this, [ 'message' => $message, @@ -721,22 +753,6 @@ protected function loadMessagesFromPatterns($textDomain, $locale) return $messagesLoaded; } - private function storeTextDomain(string $textDomain, string $locale, ?TextDomain $loaded): void - { - if (! $loaded instanceof TextDomain) { - return; - } - - if ( - isset($this->messages[$textDomain][$locale]) && - $this->messages[$textDomain][$locale] instanceof TextDomain - ) { - $this->messages[$textDomain][$locale]->merge($loaded); - } else { - $this->messages[$textDomain][$locale] = $loaded; - } - } - /** * Load messages from files. * @@ -856,6 +872,22 @@ public function setPlaceholder(PlaceholderInterface $placeholder): void $this->placeholder = $placeholder; } + protected function storeTextDomain(string $textDomain, string $locale, ?TextDomain $loaded): void + { + if (! $loaded instanceof TextDomain) { + return; + } + + if ( + isset($this->messages[$textDomain][$locale]) && + $this->messages[$textDomain][$locale] instanceof TextDomain + ) { + $this->messages[$textDomain][$locale]->merge($loaded); + } else { + $this->messages[$textDomain][$locale] = $loaded; + } + } + /** * @param iterable $placeholders */ diff --git a/src/Translator/TranslatorWithParamsInterface.php b/src/Translator/TranslatorWithParamsInterface.php new file mode 100644 index 00000000..eb806507 --- /dev/null +++ b/src/Translator/TranslatorWithParamsInterface.php @@ -0,0 +1,31 @@ + $params + */ + public function translateWithParams( + string $message, + iterable $params = [], + string $textDomain = 'default', + ?string $locale = null + ): string; + + /** + * Translate a plural message. + * + * @param iterable $params + */ + public function translatePluralWithParams( + string $singular, + string $plural, + iterable $params = [], + string $textDomain = 'default', + ?string $locale = null + ): string; +} diff --git a/src/View/Helper/TranslatePluralWithParams.php b/src/View/Helper/TranslatePluralWithParams.php new file mode 100644 index 00000000..80e265b1 --- /dev/null +++ b/src/View/Helper/TranslatePluralWithParams.php @@ -0,0 +1,40 @@ + $params + */ + public function __invoke( + string $singular, + string $plural, + iterable $params = [], + ?string $textDomain = null, + ?string $locale = null + ): string { + $translator = $this->getTranslator(); + if (null === $translator) { + throw new Exception\RuntimeException('Translator has not been set'); + } + if (! $translator instanceof TranslatorWithParamsInterface) { + throw new Exception\RuntimeException( + 'No param support, the translator does not implement TranslatorWithParamsInterface' + ); + } + if (null === $textDomain) { + $textDomain = $this->getTranslatorTextDomain(); + } + + return $translator->translatePluralWithParams($singular, $plural, $params, $textDomain, $locale); + } +} diff --git a/src/View/Helper/TranslateWithParams.php b/src/View/Helper/TranslateWithParams.php new file mode 100644 index 00000000..6ad6e6c4 --- /dev/null +++ b/src/View/Helper/TranslateWithParams.php @@ -0,0 +1,39 @@ + $params + */ + public function __invoke( + string $message, + iterable $params = [], + ?string $textDomain = null, + ?string $locale = null + ): string { + $translator = $this->getTranslator(); + if (null === $translator) { + throw new Exception\RuntimeException('Translator has not been set'); + } + if (! $translator instanceof TranslatorWithParamsInterface) { + throw new Exception\RuntimeException( + 'No param support, the translator does not implement TranslatorWithParamsInterface' + ); + } + if (null === $textDomain) { + $textDomain = $this->getTranslatorTextDomain(); + } + + return $translator->translateWithParams($message, $params, $textDomain, $locale); + } +} From de5e91406f38f0dc3b06e30ccdd9088764ca0011 Mon Sep 17 00:00:00 2001 From: Kevin Hamilton Date: Fri, 17 May 2024 17:34:19 +0100 Subject: [PATCH 10/18] docblock tweaks Signed-off-by: Kevin Hamilton --- src/View/Helper/TranslatePluralWithParams.php | 2 +- src/View/Helper/TranslateWithParams.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/View/Helper/TranslatePluralWithParams.php b/src/View/Helper/TranslatePluralWithParams.php index 80e265b1..8d3feecf 100644 --- a/src/View/Helper/TranslatePluralWithParams.php +++ b/src/View/Helper/TranslatePluralWithParams.php @@ -6,7 +6,7 @@ use Laminas\I18n\Translator\TranslatorWithParamsInterface; /** - * View helper for translating messages. + * View helper for translating messages with placeholders. */ class TranslatePluralWithParams extends AbstractTranslatorHelper { diff --git a/src/View/Helper/TranslateWithParams.php b/src/View/Helper/TranslateWithParams.php index 6ad6e6c4..42e26cfd 100644 --- a/src/View/Helper/TranslateWithParams.php +++ b/src/View/Helper/TranslateWithParams.php @@ -6,7 +6,7 @@ use Laminas\I18n\Translator\TranslatorWithParamsInterface; /** - * View helper for translating messages. + * View helper for translating messages with placeholders. */ class TranslateWithParams extends AbstractTranslatorHelper { From 48a9044d99a9ded74d0188012c8bafd766241669 Mon Sep 17 00:00:00 2001 From: Kevin Hamilton Date: Wed, 22 May 2024 09:21:12 +0100 Subject: [PATCH 11/18] Update psalm baseline and address psalm type issues Signed-off-by: Kevin Hamilton --- psalm-baseline.xml | 98 ++----------------- src/Translator/Loader/Gettext.php | 11 +-- src/Translator/LoaderPluginManager.php | 11 ++- .../PlaceholderPluginManagerFactory.php | 2 + src/Translator/Plural/Symbol.php | 2 - src/Translator/Translator.php | 46 ++++----- 6 files changed, 40 insertions(+), 130 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 94b2f0b0..1d031182 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + @@ -55,41 +55,18 @@ - - - - - - file]]> file]]> file]]> - - file, 4 * $num))]]> - file, 4 * $num))]]> - - - - - - - - - - - - - - @@ -142,13 +119,6 @@ - - - - - - - @@ -177,6 +147,11 @@ + + + + + @@ -308,10 +283,6 @@ - - - - @@ -330,92 +301,53 @@ - - - - - - - - - - - files[$textDomain][$currentLocale]]]> - files[$textDomain][$currentLocale]]]> - messages[$textDomain][$locale]]]> - messages[$textDomain][$locale]]]> - messages[$textDomain][$locale]]]> - messages[$textDomain][$locale]]]> - messages[$textDomain][$locale]]]> - messages[$textDomain][$locale][$message]]]> - messages[$textDomain][$locale][$textDomain . "\x04" . $message]]]> - - files[$textDomain][$locale]]]> - messages[$textDomain][$locale]]]> - messages[$textDomain][$locale]]]> - messages[$textDomain][$locale]]]> - messages[$textDomain][$locale]]]> - messages[$textDomain][$locale]]]> - patterns[$textDomain][]]]> - remote[$textDomain][]]]> - - - - - - - - - - - messages[$textDomain][$locale][$message]]]> messages[$textDomain][$locale][$textDomain . "\x04" . $message]]]> @@ -423,13 +355,6 @@ - - - - - - - @@ -445,20 +370,9 @@ - - - get('TranslatorPluginManager')]]> - - - - - - - - diff --git a/src/Translator/Loader/Gettext.php b/src/Translator/Loader/Gettext.php index eb8e8ea8..87d67ccb 100644 --- a/src/Translator/Loader/Gettext.php +++ b/src/Translator/Loader/Gettext.php @@ -182,15 +182,14 @@ protected function readInteger() /** * Read an integer from the current file. * - * @param int $num - * @return int + * @param int $num + * @return array */ protected function readIntegerList($num) { - if ($this->littleEndian) { - return unpack('V' . $num, fread($this->file, 4 * $num)); - } + /** @var array|false $integerList */ + $integerList = unpack(($this->littleEndian ? 'V' : 'N') . $num, fread($this->file, 4 * $num)); - return unpack('N' . $num, fread($this->file, 4 * $num)); + return $integerList === false ? [] : $integerList; } } diff --git a/src/Translator/LoaderPluginManager.php b/src/Translator/LoaderPluginManager.php index f81c8611..e7b3e479 100644 --- a/src/Translator/LoaderPluginManager.php +++ b/src/Translator/LoaderPluginManager.php @@ -126,18 +126,19 @@ public function validate($instance) * @deprecated Since 2.16.0 - This component is no longer compatible with Service Manager v2. * This method will be removed in version 3.0 * - * @param mixed $plugin + * @param mixed $instance + * @return void * @throws Exception\RuntimeException - * @psalm-assert InstanceType $plugin + * @psalm-assert InstanceType $instance */ - public function validatePlugin($plugin) + public function validatePlugin($instance) { try { - $this->validate($plugin); + $this->validate($instance); } catch (InvalidServiceException $e) { throw new Exception\RuntimeException(sprintf( 'Plugin of type %s is invalid; must implement %s or %s', - is_object($plugin) ? $plugin::class : gettype($plugin), + is_object($instance) ? $instance::class : gettype($instance), FileLoaderInterface::class, RemoteLoaderInterface::class )); diff --git a/src/Translator/PlaceholderPluginManagerFactory.php b/src/Translator/PlaceholderPluginManagerFactory.php index 61c905ed..ad38edc8 100644 --- a/src/Translator/PlaceholderPluginManagerFactory.php +++ b/src/Translator/PlaceholderPluginManagerFactory.php @@ -21,6 +21,8 @@ class PlaceholderPluginManagerFactory implements FactoryInterface * @param array|null $options * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface + * @psalm-suppress ArgumentTypeCoercion + * @psalm-suppress DeprecatedClass */ public function __invoke( ContainerInterface $container, diff --git a/src/Translator/Plural/Symbol.php b/src/Translator/Plural/Symbol.php index 7317a89d..2b913f40 100644 --- a/src/Translator/Plural/Symbol.php +++ b/src/Translator/Plural/Symbol.php @@ -127,7 +127,6 @@ public function getNullDenotation() throw new Exception\ParseException(sprintf('Syntax error: %s', $this->id)); } - /** @var callable $function */ $function = $this->nullDenotationGetter; return $function($this); } @@ -145,7 +144,6 @@ public function getLeftDenotation($left) throw new Exception\ParseException(sprintf('Unknown operator: %s', $this->id)); } - /** @var callable $function */ $function = $this->leftDenotationGetter; return $function($this, $left); } diff --git a/src/Translator/Translator.php b/src/Translator/Translator.php index 004df10d..8a17ff69 100644 --- a/src/Translator/Translator.php +++ b/src/Translator/Translator.php @@ -45,7 +45,7 @@ class Translator implements TranslatorInterface, TranslatorWithParamsInterface /** * Messages loaded by the translator. * - * @var array|TextDomain> + * @var array|null>> */ protected $messages = []; @@ -343,9 +343,9 @@ public function getPluginManager() /** * Translate a message. * - * @param string $message - * @param string|string[] $textDomain - * @param string|null $locale + * @param string $message + * @param string| $textDomain + * @param string|null $locale * @return string */ public function translate($message, $textDomain = 'default', $locale = null) @@ -370,11 +370,11 @@ public function translate($message, $textDomain = 'default', $locale = null) /** * Translate a plural message. * - * @param string $singular - * @param string $plural - * @param int $number - * @param string|string[] $textDomain - * @param string|null $locale + * @param string $singular + * @param string $plural + * @param int $number + * @param string $textDomain + * @param string|null $locale * @return string * @throws Exception\OutOfBoundsException */ @@ -393,16 +393,13 @@ public function translatePlural( } $index = $number === 1 ? 0 : 1; // en_EN Plural rule - if ( - isset($this->messages[$textDomain][$locale]) && - $this->messages[$textDomain][$locale] instanceof TextDomain - ) { + if (isset($this->messages[$textDomain][$locale])) { $index = $this->messages[$textDomain][$locale] ->getPluralRule() ->evaluate($number); } - if (isset($translation[$index]) && is_string($translation[$index]) && $translation[$index] !== '') { + if (isset($translation[$index]) && $translation[$index] !== '') { return $translation[$index]; } @@ -472,7 +469,7 @@ public function translatePluralWithParams( * @param string $message * @param string $locale * @param string $textDomain - * @return string|array|null + * @return string|null */ protected function getTranslatedMessage( $message, @@ -639,10 +636,11 @@ public function clearCache($textDomain, $locale) * Load messages for a given language and domain. * * @triggers loadMessages.no-messages-loaded - * @param string $textDomain - * @param string $locale - * @throws Exception\RuntimeException - * @return void + * @param string $textDomain + * @param string $locale + * @return void + * @throws ExceptionInterface + * @throws Exception\RuntimeException */ protected function loadMessages($textDomain, $locale) { @@ -653,8 +651,9 @@ protected function loadMessages($textDomain, $locale) $cacheId = ''; if (null !== ($cache = $this->getCache())) { $cacheId = $this->getCacheId($textDomain, $locale); - - if (null !== ($result = $cache->getItem($cacheId))) { + /** @var TextDomain|null $result */ + $result = $cache->getItem($cacheId); + if ($result instanceof TextDomain) { $this->messages[$textDomain][$locale] = $result; return; @@ -878,10 +877,7 @@ protected function storeTextDomain(string $textDomain, string $locale, ?TextDoma return; } - if ( - isset($this->messages[$textDomain][$locale]) && - $this->messages[$textDomain][$locale] instanceof TextDomain - ) { + if (isset($this->messages[$textDomain][$locale])) { $this->messages[$textDomain][$locale]->merge($loaded); } else { $this->messages[$textDomain][$locale] = $loaded; From 7ca402c0b54b103b80f5d6801341914863a66605 Mon Sep 17 00:00:00 2001 From: Kevin Hamilton Date: Wed, 22 May 2024 09:24:35 +0100 Subject: [PATCH 12/18] Fix param in docblock Signed-off-by: Kevin Hamilton --- src/Translator/Translator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Translator/Translator.php b/src/Translator/Translator.php index 8a17ff69..9625c273 100644 --- a/src/Translator/Translator.php +++ b/src/Translator/Translator.php @@ -344,7 +344,7 @@ public function getPluginManager() * Translate a message. * * @param string $message - * @param string| $textDomain + * @param string $textDomain * @param string|null $locale * @return string */ From 5c4f32d4f6eb5ce9d2c233538dfdcb7f58a9ec7b Mon Sep 17 00:00:00 2001 From: Kevin Hamilton Date: Wed, 22 May 2024 17:38:08 +0100 Subject: [PATCH 13/18] Initial re-work using decorator pattern Signed-off-by: Kevin Hamilton --- src/ConfigProvider.php | 9 +-- .../Formatter/FormatterInterface.php | 13 ++++ .../HandlebarFormatter.php} | 6 +- .../IcuFormatter.php} | 6 +- .../PrintfFormatter.php} | 6 +- .../SegmentFormatter.php} | 6 +- ...Manager.php => FormatterPluginManager.php} | 36 +++++----- ....php => FormatterPluginManagerFactory.php} | 13 ++-- .../Placeholder/PlaceholderInterface.php | 13 ---- src/Translator/Translator.php | 68 +------------------ .../TranslatorFormatterDecorator.php | 64 +++++++++++++++++ .../TranslatorFormatterDecoratorFactory.php | 65 ++++++++++++++++++ src/Translator/TranslatorServiceFactory.php | 31 --------- .../TranslatorWithParamsInterface.php | 31 --------- src/View/Helper/TranslatePluralWithParams.php | 11 +-- src/View/Helper/TranslateWithParams.php | 11 +-- .../TranslatorServiceFactoryTest.php | 35 ++-------- 17 files changed, 200 insertions(+), 224 deletions(-) create mode 100644 src/Translator/Formatter/FormatterInterface.php rename src/Translator/{Placeholder/HandlebarPlaceholder.php => Formatter/HandlebarFormatter.php} (56%) rename src/Translator/{Placeholder/IcuPlaceholder.php => Formatter/IcuFormatter.php} (62%) rename src/Translator/{Placeholder/PrintfPlaceholder.php => Formatter/PrintfFormatter.php} (76%) rename src/Translator/{Placeholder/SegmentPlaceholder.php => Formatter/SegmentFormatter.php} (89%) rename src/Translator/{PlaceholderPluginManager.php => FormatterPluginManager.php} (54%) rename src/Translator/{PlaceholderPluginManagerFactory.php => FormatterPluginManagerFactory.php} (74%) delete mode 100644 src/Translator/Placeholder/PlaceholderInterface.php create mode 100644 src/Translator/TranslatorFormatterDecorator.php create mode 100644 src/Translator/TranslatorFormatterDecoratorFactory.php delete mode 100644 src/Translator/TranslatorWithParamsInterface.php diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index bebeb8d9..c3c89dea 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -52,10 +52,11 @@ public function getDependencyConfig() Geography\CountryCodeListInterface::class => Geography\DefaultCountryCodeList::class, ], 'factories' => [ - Translator\TranslatorInterface::class => Translator\TranslatorServiceFactory::class, - Translator\LoaderPluginManager::class => Translator\LoaderPluginManagerFactory::class, - Translator\PlaceholderPluginManager::class => Translator\PlaceholderPluginManagerFactory::class, - Geography\DefaultCountryCodeList::class => [Geography\DefaultCountryCodeList::class, 'create'], + Translator\TranslatorInterface::class => Translator\TranslatorServiceFactory::class, + Translator\LoaderPluginManager::class => Translator\LoaderPluginManagerFactory::class, + Translator\FormatterPluginManager::class => Translator\FormatterPluginManagerFactory::class, + Translator\TranslatorFormatterDecorator::class => Translator\TranslatorFormatterDecoratorFactory::class, + Geography\DefaultCountryCodeList::class => [Geography\DefaultCountryCodeList::class, 'create'], ], ]; } diff --git a/src/Translator/Formatter/FormatterInterface.php b/src/Translator/Formatter/FormatterInterface.php new file mode 100644 index 00000000..f7b62b66 --- /dev/null +++ b/src/Translator/Formatter/FormatterInterface.php @@ -0,0 +1,13 @@ + $placeholders + */ + public function format(string $locale, string $message, iterable $placeholders = []): string; +} diff --git a/src/Translator/Placeholder/HandlebarPlaceholder.php b/src/Translator/Formatter/HandlebarFormatter.php similarity index 56% rename from src/Translator/Placeholder/HandlebarPlaceholder.php rename to src/Translator/Formatter/HandlebarFormatter.php index bbcac635..8fe33e00 100644 --- a/src/Translator/Placeholder/HandlebarPlaceholder.php +++ b/src/Translator/Formatter/HandlebarFormatter.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace Laminas\I18n\Translator\Placeholder; +namespace Laminas\I18n\Translator\Formatter; use function str_replace; -class HandlebarPlaceholder implements PlaceholderInterface +class HandlebarFormatter implements FormatterInterface { - public function compile(string $locale, string $message, iterable $placeholders = []): string + public function format(string $locale, string $message, iterable $placeholders = []): string { $compiled = $message; foreach ($placeholders as $key => $value) { diff --git a/src/Translator/Placeholder/IcuPlaceholder.php b/src/Translator/Formatter/IcuFormatter.php similarity index 62% rename from src/Translator/Placeholder/IcuPlaceholder.php rename to src/Translator/Formatter/IcuFormatter.php index d0f6609b..f45e1d25 100644 --- a/src/Translator/Placeholder/IcuPlaceholder.php +++ b/src/Translator/Formatter/IcuFormatter.php @@ -2,16 +2,16 @@ declare(strict_types=1); -namespace Laminas\I18n\Translator\Placeholder; +namespace Laminas\I18n\Translator\Formatter; use MessageFormatter; use Traversable; use function iterator_to_array; -class IcuPlaceholder implements PlaceholderInterface +class IcuFormatter implements FormatterInterface { - public function compile(string $locale, string $message, iterable $placeholders = []): string + public function format(string $locale, string $message, iterable $placeholders = []): string { if ($placeholders instanceof Traversable) { $placeholders = iterator_to_array($placeholders); diff --git a/src/Translator/Placeholder/PrintfPlaceholder.php b/src/Translator/Formatter/PrintfFormatter.php similarity index 76% rename from src/Translator/Placeholder/PrintfPlaceholder.php rename to src/Translator/Formatter/PrintfFormatter.php index 7e32c64a..95058234 100644 --- a/src/Translator/Placeholder/PrintfPlaceholder.php +++ b/src/Translator/Formatter/PrintfFormatter.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Laminas\I18n\Translator\Placeholder; +namespace Laminas\I18n\Translator\Formatter; use Laminas\I18n\Exception\ParseException; use Traversable; @@ -10,9 +10,9 @@ use function call_user_func_array; use function iterator_to_array; -class PrintfPlaceholder implements PlaceholderInterface +class PrintfFormatter implements FormatterInterface { - public function compile(string $locale, string $message, iterable $placeholders = []): string + public function format(string $locale, string $message, iterable $placeholders = []): string { if ($placeholders instanceof Traversable) { $placeholders = iterator_to_array($placeholders); diff --git a/src/Translator/Placeholder/SegmentPlaceholder.php b/src/Translator/Formatter/SegmentFormatter.php similarity index 89% rename from src/Translator/Placeholder/SegmentPlaceholder.php rename to src/Translator/Formatter/SegmentFormatter.php index cc96cfe8..f8138e78 100644 --- a/src/Translator/Placeholder/SegmentPlaceholder.php +++ b/src/Translator/Formatter/SegmentFormatter.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Laminas\I18n\Translator\Placeholder; +namespace Laminas\I18n\Translator\Formatter; use Laminas\I18n\Exception\InvalidArgumentException; use Laminas\I18n\Exception\ParseException; @@ -17,9 +17,9 @@ use function ucfirst; use function uksort; -class SegmentPlaceholder implements PlaceholderInterface +class SegmentFormatter implements FormatterInterface { - public function compile(string $locale, string $message, iterable $placeholders = []): string + public function format(string $locale, string $message, iterable $placeholders = []): string { if ($placeholders instanceof Traversable) { $placeholders = iterator_to_array($placeholders); diff --git a/src/Translator/PlaceholderPluginManager.php b/src/Translator/FormatterPluginManager.php similarity index 54% rename from src/Translator/PlaceholderPluginManager.php rename to src/Translator/FormatterPluginManager.php index 802bb372..3484be16 100644 --- a/src/Translator/PlaceholderPluginManager.php +++ b/src/Translator/FormatterPluginManager.php @@ -19,31 +19,31 @@ * Placeholder\PlaceholderInterface. Additionally, it registers a number * of default placeholder compilers. * - * @template InstanceType of Placeholder\PlaceholderInterface + * @template InstanceType of Formatter\FormatterInterface * @extends AbstractPluginManager - * @method Placeholder\PlaceholderInterface get(string $name, ?array $options = null) + * @method Formatter\FormatterInterface get(string $name, ?array $options = null) */ -class PlaceholderPluginManager extends AbstractPluginManager +class FormatterPluginManager extends AbstractPluginManager { /** @inheritDoc */ protected $aliases = [ - 'segment' => Placeholder\SegmentPlaceholder::class, - 'colon' => Placeholder\SegmentPlaceholder::class, - 'laravel' => Placeholder\SegmentPlaceholder::class, - 'handlebar' => Placeholder\HandlebarPlaceholder::class, - 'handlebars' => Placeholder\HandlebarPlaceholder::class, - 'icu' => Placeholder\IcuPlaceholder::class, - 'vsprintf' => Placeholder\PrintfPlaceholder::class, - 'sprintf' => Placeholder\PrintfPlaceholder::class, - 'printf' => Placeholder\PrintfPlaceholder::class, + 'segment' => Formatter\SegmentFormatter::class, + 'colon' => Formatter\SegmentFormatter::class, + 'laravel' => Formatter\SegmentFormatter::class, + 'handlebar' => Formatter\HandlebarFormatter::class, + 'handlebars' => Formatter\HandlebarFormatter::class, + 'icu' => Formatter\IcuFormatter::class, + 'vsprintf' => Formatter\PrintfFormatter::class, + 'sprintf' => Formatter\PrintfFormatter::class, + 'printf' => Formatter\PrintfFormatter::class, ]; /** @inheritDoc */ protected $factories = [ - Placeholder\SegmentPlaceholder::class => InvokableFactory::class, - Placeholder\HandlebarPlaceholder::class => InvokableFactory::class, - Placeholder\IcuPlaceholder::class => InvokableFactory::class, - Placeholder\PrintfPlaceholder::class => InvokableFactory::class, + Formatter\SegmentFormatter::class => InvokableFactory::class, + Formatter\HandlebarFormatter::class => InvokableFactory::class, + Formatter\IcuFormatter::class => InvokableFactory::class, + Formatter\PrintfFormatter::class => InvokableFactory::class, ]; /** @@ -57,7 +57,7 @@ class PlaceholderPluginManager extends AbstractPluginManager */ public function validate(mixed $instance): void { - if ($instance instanceof Placeholder\PlaceholderInterface) { + if ($instance instanceof Formatter\FormatterInterface) { // we're okay return; } @@ -65,7 +65,7 @@ public function validate(mixed $instance): void throw new InvalidServiceException(sprintf( 'Plugin of type %s is invalid; must implement %s', is_object($instance) ? $instance::class : gettype($instance), - Placeholder\PlaceholderInterface::class + Formatter\FormatterInterface::class )); } } diff --git a/src/Translator/PlaceholderPluginManagerFactory.php b/src/Translator/FormatterPluginManagerFactory.php similarity index 74% rename from src/Translator/PlaceholderPluginManagerFactory.php rename to src/Translator/FormatterPluginManagerFactory.php index ad38edc8..98629a14 100644 --- a/src/Translator/PlaceholderPluginManagerFactory.php +++ b/src/Translator/FormatterPluginManagerFactory.php @@ -12,25 +12,22 @@ use function is_array; /** @psalm-import-type ServiceManagerConfiguration from ServiceManager */ -class PlaceholderPluginManagerFactory implements FactoryInterface +final class FormatterPluginManagerFactory implements FactoryInterface { /** - * Create and return a PlaceholderPluginManager. - * * @param string $requestedName * @param array|null $options * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface - * @psalm-suppress ArgumentTypeCoercion * @psalm-suppress DeprecatedClass */ public function __invoke( ContainerInterface $container, $requestedName, ?array $options = null - ): PlaceholderPluginManager { + ): FormatterPluginManager { $options ??= []; - $pluginManager = new PlaceholderPluginManager($container, $options); + $pluginManager = new FormatterPluginManager($container, $options); // If this is in a laminas-mvc application, the ServiceListener will inject // merged configuration during bootstrap. @@ -46,12 +43,12 @@ public function __invoke( $config = $container->get('config'); // If we do not have translator_plugins configuration, nothing more to do - if (! isset($config['translator_placeholders']) || ! is_array($config['translator_placeholders'])) { + if (! isset($config['translator_formatter']) || ! is_array($config['translator_formatter'])) { return $pluginManager; } // Wire service configuration for translator_plugins - (new Config($config['translator_placeholders']))->configureServiceManager($pluginManager); + (new Config($config['translator_formatter']))->configureServiceManager($pluginManager); return $pluginManager; } diff --git a/src/Translator/Placeholder/PlaceholderInterface.php b/src/Translator/Placeholder/PlaceholderInterface.php deleted file mode 100644 index 5c653f53..00000000 --- a/src/Translator/Placeholder/PlaceholderInterface.php +++ /dev/null @@ -1,13 +0,0 @@ - $placeholders - */ - public function compile(string $locale, string $message, iterable $placeholders = []): string; -} diff --git a/src/Translator/Translator.php b/src/Translator/Translator.php index 9625c273..f3f9545b 100644 --- a/src/Translator/Translator.php +++ b/src/Translator/Translator.php @@ -11,14 +11,12 @@ use Laminas\I18n\Exception; use Laminas\I18n\Translator\Loader\FileLoaderInterface; use Laminas\I18n\Translator\Loader\RemoteLoaderInterface; -use Laminas\I18n\Translator\Placeholder\PlaceholderInterface; use Laminas\ServiceManager\ServiceManager; use Laminas\Stdlib\ArrayUtils; use Locale; use Traversable; use function array_shift; -use function ctype_digit; use function get_debug_type; use function is_array; use function is_file; @@ -30,7 +28,7 @@ /** * Translator. */ -class Translator implements TranslatorInterface, TranslatorWithParamsInterface +class Translator implements TranslatorInterface { /** * Event fired when the translation for a message is missing. @@ -112,8 +110,6 @@ class Translator implements TranslatorInterface, TranslatorWithParamsInterface */ protected $eventsEnabled = false; - protected ?PlaceholderInterface $placeholder = null; - /** * Instantiate a translator * @@ -419,49 +415,6 @@ public function translatePlural( return $index === 0 ? $singular : $plural; } - /** - * @param iterable $params - */ - public function translateWithParams( - string $message, - iterable $params = [], - string $textDomain = 'default', - ?string $locale = null - ): string { - $locale ??= $this->getLocale(); - - return $this->compileMessage($this->translate($message, $textDomain, $locale), $params, $locale); - } - - /** - * The first number in params is used to determine the plural form. - * - * @param iterable $params - */ - public function translatePluralWithParams( - string $singular, - string $plural, - iterable $params = [], - string $textDomain = 'default', - ?string $locale = null - ): string { - $locale ??= $this->getLocale(); - - $number = 1; - foreach ($params as $param) { - if (ctype_digit($param)) { - $number = (int) $param; - break; - } - } - - return $this->compileMessage( - $this->translatePlural($singular, $plural, $number, $textDomain, $locale), - $params, - $locale - ); - } - /** * Get a translated message. * @@ -866,11 +819,6 @@ public function disableEventManager() return $this; } - public function setPlaceholder(PlaceholderInterface $placeholder): void - { - $this->placeholder = $placeholder; - } - protected function storeTextDomain(string $textDomain, string $locale, ?TextDomain $loaded): void { if (! $loaded instanceof TextDomain) { @@ -883,18 +831,4 @@ protected function storeTextDomain(string $textDomain, string $locale, ?TextDoma $this->messages[$textDomain][$locale] = $loaded; } } - - /** - * @param iterable $placeholders - */ - protected function compileMessage(?string $message, iterable $placeholders, string $locale): string - { - return $this->placeholder && $message !== '' && $message !== null ? - $this->placeholder->compile( - $locale, - $message, - $placeholders - ) : - ($message ?? ''); - } } diff --git a/src/Translator/TranslatorFormatterDecorator.php b/src/Translator/TranslatorFormatterDecorator.php new file mode 100644 index 00000000..3663fc25 --- /dev/null +++ b/src/Translator/TranslatorFormatterDecorator.php @@ -0,0 +1,64 @@ + $params + */ + public function translate( + $message, + $textDomain = 'default', + $locale = null, + iterable $params = [] + ): string { + $locale ??= $this->translator->getLocale(); + + return $this->formatMessage($this->translator->translate($message, $textDomain, $locale), $params, $locale); + } + + /** + * @param string $singular + * @param string $plural + * @param int $number + * @param string $textDomain + * @param string $locale + * @param iterable $params + */ + public function translatePlural( + $singular, + $plural, + $number, + $textDomain = 'default', + $locale = null, + iterable $params = [] + ): string { + $locale ??= $this->translator->getLocale(); + + return $this->formatMessage( + $this->translatePlural($singular, $plural, $number, $textDomain, $locale), + $params, + $locale + ); + } + + /** + * @param iterable $placeholders + */ + protected function formatMessage(string $message, iterable $placeholders, string $locale): string + { + return $message !== '' ? $this->formatter->format($locale, $message, $placeholders) : $message; + } +} diff --git a/src/Translator/TranslatorFormatterDecoratorFactory.php b/src/Translator/TranslatorFormatterDecoratorFactory.php new file mode 100644 index 00000000..f906786d --- /dev/null +++ b/src/Translator/TranslatorFormatterDecoratorFactory.php @@ -0,0 +1,65 @@ + $config */ + $config = $container->get('config'); + $trConfig = $config['translator'] ?? []; + $translator = $container->get(TranslatorInterface::class); + + /** @var FormatterPluginManager $formatterPluginManager */ + $formatterPluginManager = $container->get(FormatterPluginManager::class); + /** @var string|FormatterInterface|mixed $formatterName */ + $formatterName = $trConfig['message_format'] ?? 'handlebars'; + if ($formatterName instanceof FormatterInterface) { + $formatter = $formatterName; + } elseif (is_string($formatterName)) { + if (! $formatterPluginManager->has($formatterName)) { + throw new ServiceNotCreatedException( + sprintf('Could not find a placeholder format with the name "%s"', $formatterName) + ); + } + + $formatter = $formatterPluginManager->get($formatterName); + } else { + throw new InvalidServiceException(sprintf( + '\'message_format\' of type %s is invalid; must be a string or object that implements %s', + is_object($formatterName) ? $formatterName::class : gettype($formatterName), + FormatterInterface::class + )); + } + + return new TranslatorFormatterDecorator($translator, $formatter); + } +} diff --git a/src/Translator/TranslatorServiceFactory.php b/src/Translator/TranslatorServiceFactory.php index 3830bb1e..0c45e223 100644 --- a/src/Translator/TranslatorServiceFactory.php +++ b/src/Translator/TranslatorServiceFactory.php @@ -2,18 +2,11 @@ namespace Laminas\I18n\Translator; -use Laminas\ServiceManager\Exception\InvalidServiceException; -use Laminas\ServiceManager\Exception\ServiceNotCreatedException; use Laminas\ServiceManager\Factory\FactoryInterface; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; -use function gettype; -use function is_object; -use function is_string; -use function sprintf; - /** * Translator. */ @@ -37,30 +30,6 @@ public function __invoke(ContainerInterface $container, $requestedName, ?array $ $translator->setPluginManager($container->get('TranslatorPluginManager')); } - /** @var PlaceholderPluginManager $placeholderManager */ - $placeholderManager = $container->get(PlaceholderPluginManager::class); - /** @var mixed $placeholderName */ - $placeholderName = $trConfig['placeholder_format'] ?? 'handlebars'; - if ($placeholderName instanceof Placeholder\PlaceholderInterface) { - $placeholder = $placeholderName; - } elseif (is_string($placeholderName)) { - if (! $placeholderManager->has($placeholderName)) { - throw new ServiceNotCreatedException( - sprintf('Could not find a placeholder format with the name "%s"', $placeholderName) - ); - } - - $placeholder = $placeholderManager->get($placeholderName); - } else { - throw new InvalidServiceException(sprintf( - '\'placeholder_format\' of type %s is invalid; must be a string or object that implements %s', - is_object($placeholderName) ? $placeholderName::class : gettype($placeholderName), - Placeholder\PlaceholderInterface::class - )); - } - - $translator->setPlaceholder($placeholder); - return $translator; } } diff --git a/src/Translator/TranslatorWithParamsInterface.php b/src/Translator/TranslatorWithParamsInterface.php deleted file mode 100644 index eb806507..00000000 --- a/src/Translator/TranslatorWithParamsInterface.php +++ /dev/null @@ -1,31 +0,0 @@ - $params - */ - public function translateWithParams( - string $message, - iterable $params = [], - string $textDomain = 'default', - ?string $locale = null - ): string; - - /** - * Translate a plural message. - * - * @param iterable $params - */ - public function translatePluralWithParams( - string $singular, - string $plural, - iterable $params = [], - string $textDomain = 'default', - ?string $locale = null - ): string; -} diff --git a/src/View/Helper/TranslatePluralWithParams.php b/src/View/Helper/TranslatePluralWithParams.php index 8d3feecf..88cf5e20 100644 --- a/src/View/Helper/TranslatePluralWithParams.php +++ b/src/View/Helper/TranslatePluralWithParams.php @@ -3,6 +3,7 @@ namespace Laminas\I18n\View\Helper; use Laminas\I18n\Exception; +use Laminas\I18n\Translator\TranslatorFormatterDecorator; use Laminas\I18n\Translator\TranslatorWithParamsInterface; /** @@ -14,19 +15,21 @@ class TranslatePluralWithParams extends AbstractTranslatorHelper * Translate a message * * @param iterable $params + * @noinspection PhpTooManyParametersInspection */ public function __invoke( string $singular, string $plural, - iterable $params = [], + int $number, ?string $textDomain = null, - ?string $locale = null + ?string $locale = null, + iterable $params = [], ): string { $translator = $this->getTranslator(); if (null === $translator) { throw new Exception\RuntimeException('Translator has not been set'); } - if (! $translator instanceof TranslatorWithParamsInterface) { + if (! $translator instanceof TranslatorFormatterDecorator) { throw new Exception\RuntimeException( 'No param support, the translator does not implement TranslatorWithParamsInterface' ); @@ -35,6 +38,6 @@ public function __invoke( $textDomain = $this->getTranslatorTextDomain(); } - return $translator->translatePluralWithParams($singular, $plural, $params, $textDomain, $locale); + return $translator->translatePlural($singular, $plural, $number, $textDomain, $locale, $params); } } diff --git a/src/View/Helper/TranslateWithParams.php b/src/View/Helper/TranslateWithParams.php index 42e26cfd..c7a18d80 100644 --- a/src/View/Helper/TranslateWithParams.php +++ b/src/View/Helper/TranslateWithParams.php @@ -3,6 +3,7 @@ namespace Laminas\I18n\View\Helper; use Laminas\I18n\Exception; +use Laminas\I18n\Translator\TranslatorFormatterDecorator; use Laminas\I18n\Translator\TranslatorWithParamsInterface; /** @@ -17,23 +18,23 @@ class TranslateWithParams extends AbstractTranslatorHelper */ public function __invoke( string $message, - iterable $params = [], ?string $textDomain = null, - ?string $locale = null + ?string $locale = null, + iterable $params = [] ): string { $translator = $this->getTranslator(); if (null === $translator) { throw new Exception\RuntimeException('Translator has not been set'); } - if (! $translator instanceof TranslatorWithParamsInterface) { + if (! $translator instanceof TranslatorFormatterDecorator) { throw new Exception\RuntimeException( - 'No param support, the translator does not implement TranslatorWithParamsInterface' + 'No param support, the translator must be wrapped with TranslatorFormatterDecorator' ); } if (null === $textDomain) { $textDomain = $this->getTranslatorTextDomain(); } - return $translator->translateWithParams($message, $params, $textDomain, $locale); + return $translator->translate($message, $textDomain, $locale, $params); } } diff --git a/test/Translator/TranslatorServiceFactoryTest.php b/test/Translator/TranslatorServiceFactoryTest.php index dd165953..a60681df 100644 --- a/test/Translator/TranslatorServiceFactoryTest.php +++ b/test/Translator/TranslatorServiceFactoryTest.php @@ -5,8 +5,6 @@ namespace LaminasTest\I18n\Translator; use Laminas\I18n\Translator\LoaderPluginManager; -use Laminas\I18n\Translator\Placeholder\HandlebarPlaceholder; -use Laminas\I18n\Translator\PlaceholderPluginManager; use Laminas\I18n\Translator\Translator; use Laminas\I18n\Translator\TranslatorServiceFactory; use LaminasTest\I18n\TestCase; @@ -17,7 +15,6 @@ class TranslatorServiceFactoryTest extends TestCase public function testCreateServiceWithNoTranslatorKeyDefined(): void { $pluginManagerMock = $this->createMock(LoaderPluginManager::class); - $placeholderManagerMock = $this->createMock(PlaceholderPluginManager::class); $serviceLocator = $this->createMock(ContainerInterface::class); $serviceLocator->expects(self::once()) @@ -25,21 +22,10 @@ public function testCreateServiceWithNoTranslatorKeyDefined(): void ->with('TranslatorPluginManager') ->willReturn(true); - $placeholderManagerMock->expects(self::once()) - ->method('has') - ->with('handlebars') - ->willReturn(true); - - $placeholderManagerMock->expects(self::once()) - ->method('get') - ->with('handlebars') - ->willReturn(new HandlebarPlaceholder()); - - $serviceLocator->expects(self::exactly(3)) + $serviceLocator->expects(self::exactly(2)) ->method('get') ->willReturnMap([ ['TranslatorPluginManager', $pluginManagerMock], - [PlaceholderPluginManager::class, $placeholderManagerMock], ['config', []], ]); @@ -57,23 +43,10 @@ public function testCreateServiceWithNoTranslatorPluginManagerDefined(): void ->with('TranslatorPluginManager') ->willReturn(false); - $placeholderManagerMock = $this->createMock(PlaceholderPluginManager::class); - $serviceLocator->expects(self::exactly(2)) - ->method('get') - ->willReturnMap([ - [PlaceholderPluginManager::class, $placeholderManagerMock], - ['config', []], - ]); - - $placeholderManagerMock->expects(self::once()) - ->method('has') - ->with('handlebars') - ->willReturn(true); - - $placeholderManagerMock->expects(self::once()) + $serviceLocator->expects(self::once()) ->method('get') - ->with('handlebars') - ->willReturn(new HandlebarPlaceholder()); + ->with('config') + ->willReturn([]); $factory = new TranslatorServiceFactory(); $translator = $factory($serviceLocator, Translator::class); From da1f695470b08f991fb091a95eb67fdf6554e44e Mon Sep 17 00:00:00 2001 From: Kevin Hamilton Date: Thu, 23 May 2024 10:55:01 +0100 Subject: [PATCH 14/18] Better type inference for psalm, updated pslam baseline Signed-off-by: Kevin Hamilton --- psalm-baseline.xml | 10 ++++-- .../Formatter/FormatterInterface.php | 4 +-- .../Formatter/HandlebarFormatter.php | 4 +-- src/Translator/Formatter/IcuFormatter.php | 8 ++--- src/Translator/Formatter/PrintfFormatter.php | 8 ++--- src/Translator/Formatter/SegmentFormatter.php | 14 ++++---- src/Translator/FormatterPluginManager.php | 9 +++-- .../FormatterPluginManagerFactory.php | 3 +- .../TranslatorFormatterDecorator.php | 36 +++++++++++++++---- src/View/Helper/TranslatePluralWithParams.php | 1 - src/View/Helper/TranslateWithParams.php | 1 - 11 files changed, 61 insertions(+), 37 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 1d031182..c0e89ab0 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -147,10 +147,16 @@ - + + + + - + + + + diff --git a/src/Translator/Formatter/FormatterInterface.php b/src/Translator/Formatter/FormatterInterface.php index f7b62b66..2f5dabc9 100644 --- a/src/Translator/Formatter/FormatterInterface.php +++ b/src/Translator/Formatter/FormatterInterface.php @@ -7,7 +7,7 @@ interface FormatterInterface { /** - * @param iterable $placeholders + * @param iterable $params */ - public function format(string $locale, string $message, iterable $placeholders = []): string; + public function format(string $locale, string $message, iterable $params = []): string; } diff --git a/src/Translator/Formatter/HandlebarFormatter.php b/src/Translator/Formatter/HandlebarFormatter.php index 8fe33e00..e89aa8d5 100644 --- a/src/Translator/Formatter/HandlebarFormatter.php +++ b/src/Translator/Formatter/HandlebarFormatter.php @@ -8,10 +8,10 @@ class HandlebarFormatter implements FormatterInterface { - public function format(string $locale, string $message, iterable $placeholders = []): string + public function format(string $locale, string $message, iterable $params = []): string { $compiled = $message; - foreach ($placeholders as $key => $value) { + foreach ($params as $key => $value) { $compiled = str_replace("{{{$key}}}", $value, $compiled); } diff --git a/src/Translator/Formatter/IcuFormatter.php b/src/Translator/Formatter/IcuFormatter.php index f45e1d25..b92f1de3 100644 --- a/src/Translator/Formatter/IcuFormatter.php +++ b/src/Translator/Formatter/IcuFormatter.php @@ -11,12 +11,12 @@ class IcuFormatter implements FormatterInterface { - public function format(string $locale, string $message, iterable $placeholders = []): string + public function format(string $locale, string $message, iterable $params = []): string { - if ($placeholders instanceof Traversable) { - $placeholders = iterator_to_array($placeholders); + if ($params instanceof Traversable) { + $params = iterator_to_array($params); } - return MessageFormatter::formatMessage($locale, $message, $placeholders); + return MessageFormatter::formatMessage($locale, $message, $params); } } diff --git a/src/Translator/Formatter/PrintfFormatter.php b/src/Translator/Formatter/PrintfFormatter.php index 95058234..a8d415af 100644 --- a/src/Translator/Formatter/PrintfFormatter.php +++ b/src/Translator/Formatter/PrintfFormatter.php @@ -12,14 +12,14 @@ class PrintfFormatter implements FormatterInterface { - public function format(string $locale, string $message, iterable $placeholders = []): string + public function format(string $locale, string $message, iterable $params = []): string { - if ($placeholders instanceof Traversable) { - $placeholders = iterator_to_array($placeholders); + if ($params instanceof Traversable) { + $params = iterator_to_array($params); } /** @var string|false $compiled */ - $compiled = call_user_func_array('vsprintf', [$message, $placeholders]); + $compiled = call_user_func_array('vsprintf', [$message, $params]); if ($compiled === false) { throw new ParseException( 'Error occurred while processing sprintf placeholders for message "' . $message . '"' diff --git a/src/Translator/Formatter/SegmentFormatter.php b/src/Translator/Formatter/SegmentFormatter.php index f8138e78..1129c898 100644 --- a/src/Translator/Formatter/SegmentFormatter.php +++ b/src/Translator/Formatter/SegmentFormatter.php @@ -19,17 +19,17 @@ class SegmentFormatter implements FormatterInterface { - public function format(string $locale, string $message, iterable $placeholders = []): string + public function format(string $locale, string $message, iterable $params = []): string { - if ($placeholders instanceof Traversable) { - $placeholders = iterator_to_array($placeholders); + if ($params instanceof Traversable) { + $params = iterator_to_array($params); } - if (empty($placeholders)) { + if (empty($params)) { return $message; } - if (! ArrayUtils::hasStringKeys($placeholders)) { + if (! ArrayUtils::hasStringKeys($params)) { throw new InvalidArgumentException( 'SegmentPlaceholder expects an associative array of placeholder names and values' ); @@ -38,12 +38,12 @@ public function format(string $locale, string $message, iterable $placeholders = try { // Sorting the array by key length to replace placeholders with longer names first // to avoid replacing placeholders with shorter names that are part of longer names - uksort($placeholders, static function (string|int $a, string|int $b) { + uksort($params, static function (string|int $a, string|int $b) { return strlen((string) $a) <=> strlen((string) $b); }); $compiled = $message; - foreach ($placeholders as $key => $value) { + foreach ($params as $key => $value) { $key = (string) $key; $compiled = str_replace([':' . $key, ':' . strtoupper($key), ':' . ucfirst($key)], [ $value, diff --git a/src/Translator/FormatterPluginManager.php b/src/Translator/FormatterPluginManager.php index 3484be16..e48c0eb3 100644 --- a/src/Translator/FormatterPluginManager.php +++ b/src/Translator/FormatterPluginManager.php @@ -13,11 +13,11 @@ use function sprintf; /** - * Plugin manager implementation for translation placeholder compilers. + * Plugin manager implementation for translation message formatters. * * Enforces that placeholder compilers retrieved are either instances of - * Placeholder\PlaceholderInterface. Additionally, it registers a number - * of default placeholder compilers. + * Formatter\FormatterInterface. Additionally, it registers a number + * of default message formatters. * * @template InstanceType of Formatter\FormatterInterface * @extends AbstractPluginManager @@ -49,8 +49,7 @@ class FormatterPluginManager extends AbstractPluginManager /** * Validate the plugin. * - * Checks that the filter loaded is an instance of - * Loader\FileLoaderInterface or Loader\RemoteLoaderInterface. + * Checks that the filter loaded is an instance of Formatter\FormatterInterface * * @throws Exception\RuntimeException If invalid. * @psalm-assert RemoteLoaderInterface $instance diff --git a/src/Translator/FormatterPluginManagerFactory.php b/src/Translator/FormatterPluginManagerFactory.php index 98629a14..d7e4c2b5 100644 --- a/src/Translator/FormatterPluginManagerFactory.php +++ b/src/Translator/FormatterPluginManagerFactory.php @@ -16,10 +16,9 @@ final class FormatterPluginManagerFactory implements FactoryInterface { /** * @param string $requestedName - * @param array|null $options + * @psalm-param ServiceManagerConfiguration|null $options * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface - * @psalm-suppress DeprecatedClass */ public function __invoke( ContainerInterface $container, diff --git a/src/Translator/TranslatorFormatterDecorator.php b/src/Translator/TranslatorFormatterDecorator.php index 3663fc25..70358904 100644 --- a/src/Translator/TranslatorFormatterDecorator.php +++ b/src/Translator/TranslatorFormatterDecorator.php @@ -3,6 +3,10 @@ namespace Laminas\I18n\Translator; use Laminas\I18n\Translator\Formatter\FormatterInterface; +use Locale; + +use function is_string; +use function method_exists; final class TranslatorFormatterDecorator implements TranslatorInterface { @@ -24,7 +28,9 @@ public function translate( $locale = null, iterable $params = [] ): string { - $locale ??= $this->translator->getLocale(); + if ($locale === null) { + $locale = $this->getLocale(); + } return $this->formatMessage($this->translator->translate($message, $textDomain, $locale), $params, $locale); } @@ -33,8 +39,8 @@ public function translate( * @param string $singular * @param string $plural * @param int $number - * @param string $textDomain - * @param string $locale + * @param string|null $textDomain + * @param string|null $locale * @param iterable $params */ public function translatePlural( @@ -45,7 +51,9 @@ public function translatePlural( $locale = null, iterable $params = [] ): string { - $locale ??= $this->translator->getLocale(); + if ($locale === null) { + $locale = $this->getLocale(); + } return $this->formatMessage( $this->translatePlural($singular, $plural, $number, $textDomain, $locale), @@ -55,10 +63,24 @@ public function translatePlural( } /** - * @param iterable $placeholders + * @param iterable $params */ - protected function formatMessage(string $message, iterable $placeholders, string $locale): string + protected function formatMessage(string $message, iterable $params, string $locale): string { - return $message !== '' ? $this->formatter->format($locale, $message, $placeholders) : $message; + return $message !== '' ? $this->formatter->format($locale, $message, $params) : $message; + } + + protected function getLocale(): string + { + $locale = null; + if (method_exists($this->translator, 'getLocale')) { + /** @var string|null $translatorLocale */ + $translatorLocale = $this->translator->getLocale(); + if (is_string($translatorLocale)) { + $locale = $translatorLocale; + } + } + + return $locale ?? Locale::getDefault(); } } diff --git a/src/View/Helper/TranslatePluralWithParams.php b/src/View/Helper/TranslatePluralWithParams.php index 88cf5e20..6f67a331 100644 --- a/src/View/Helper/TranslatePluralWithParams.php +++ b/src/View/Helper/TranslatePluralWithParams.php @@ -4,7 +4,6 @@ use Laminas\I18n\Exception; use Laminas\I18n\Translator\TranslatorFormatterDecorator; -use Laminas\I18n\Translator\TranslatorWithParamsInterface; /** * View helper for translating messages with placeholders. diff --git a/src/View/Helper/TranslateWithParams.php b/src/View/Helper/TranslateWithParams.php index c7a18d80..5643c7d6 100644 --- a/src/View/Helper/TranslateWithParams.php +++ b/src/View/Helper/TranslateWithParams.php @@ -4,7 +4,6 @@ use Laminas\I18n\Exception; use Laminas\I18n\Translator\TranslatorFormatterDecorator; -use Laminas\I18n\Translator\TranslatorWithParamsInterface; /** * View helper for translating messages with placeholders. From 25ca826047135418fe37d87b34c5b5463b642b30 Mon Sep 17 00:00:00 2001 From: Kevin Hamilton Date: Thu, 23 May 2024 10:58:10 +0100 Subject: [PATCH 15/18] phpcs style fix Signed-off-by: Kevin Hamilton --- test/Translator/TranslatorServiceFactoryTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Translator/TranslatorServiceFactoryTest.php b/test/Translator/TranslatorServiceFactoryTest.php index a60681df..3d4ae9a2 100644 --- a/test/Translator/TranslatorServiceFactoryTest.php +++ b/test/Translator/TranslatorServiceFactoryTest.php @@ -14,7 +14,7 @@ class TranslatorServiceFactoryTest extends TestCase { public function testCreateServiceWithNoTranslatorKeyDefined(): void { - $pluginManagerMock = $this->createMock(LoaderPluginManager::class); + $pluginManagerMock = $this->createMock(LoaderPluginManager::class); $serviceLocator = $this->createMock(ContainerInterface::class); $serviceLocator->expects(self::once()) From 2e27540077c9cf085f0662d6aa023377ea838d40 Mon Sep 17 00:00:00 2001 From: Kevin Hamilton Date: Thu, 23 May 2024 15:36:03 +0100 Subject: [PATCH 16/18] Update view helper config to use new helpers with param support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Frank Brückner Signed-off-by: Kevin Hamilton --- src/ConfigProvider.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index c3c89dea..188f92ef 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -173,13 +173,11 @@ public function getViewHelperConfig() 'NumberFormat' => View\Helper\NumberFormat::class, 'plural' => View\Helper\Plural::class, 'Plural' => View\Helper\Plural::class, - 'translate' => View\Helper\Translate::class, - 'Translate' => View\Helper\Translate::class, - 'translateplural' => View\Helper\TranslatePlural::class, - 'translatePlural' => View\Helper\TranslatePlural::class, - 'TranslatePlural' => View\Helper\TranslatePlural::class, - 'translateWithParams' => View\Helper\TranslateWithParams::class, - 'translatePluralWithParams' => View\Helper\TranslatePluralWithParams::class, + 'translate' => View\Helper\TranslateWithParams::class, + 'Translate' => View\Helper\TranslateWithParams::class, + 'translateplural' => View\Helper\TranslatePluralWithParams::class, + 'translatePlural' => View\Helper\TranslatePluralWithParams::class, + 'TranslatePlural' => View\Helper\TranslatePluralWithParams::class, // Legacy Zend Framework aliases 'Zend\I18n\View\Helper\CurrencyFormat' => View\Helper\CurrencyFormat::class, From a9cc082b75ede8e5057efbbaa65e62e457a16330 Mon Sep 17 00:00:00 2001 From: Kevin Hamilton Date: Fri, 24 May 2024 09:17:08 +0100 Subject: [PATCH 17/18] Adjust some exception messages. correction for phpcs Signed-off-by: Kevin Hamilton --- src/View/Helper/TranslatePluralWithParams.php | 2 +- src/View/Helper/TranslateWithParams.php | 2 +- test/Translator/TranslatorServiceFactoryTest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/View/Helper/TranslatePluralWithParams.php b/src/View/Helper/TranslatePluralWithParams.php index 6f67a331..500cc551 100644 --- a/src/View/Helper/TranslatePluralWithParams.php +++ b/src/View/Helper/TranslatePluralWithParams.php @@ -30,7 +30,7 @@ public function __invoke( } if (! $translator instanceof TranslatorFormatterDecorator) { throw new Exception\RuntimeException( - 'No param support, the translator does not implement TranslatorWithParamsInterface' + 'No param support, the translator bust be decorated with TranslatorFormatterDecorator' ); } if (null === $textDomain) { diff --git a/src/View/Helper/TranslateWithParams.php b/src/View/Helper/TranslateWithParams.php index 5643c7d6..3a409dff 100644 --- a/src/View/Helper/TranslateWithParams.php +++ b/src/View/Helper/TranslateWithParams.php @@ -27,7 +27,7 @@ public function __invoke( } if (! $translator instanceof TranslatorFormatterDecorator) { throw new Exception\RuntimeException( - 'No param support, the translator must be wrapped with TranslatorFormatterDecorator' + 'No param support, the translator bust be decorated with TranslatorFormatterDecorator' ); } if (null === $textDomain) { diff --git a/test/Translator/TranslatorServiceFactoryTest.php b/test/Translator/TranslatorServiceFactoryTest.php index 3d4ae9a2..b9ea4df3 100644 --- a/test/Translator/TranslatorServiceFactoryTest.php +++ b/test/Translator/TranslatorServiceFactoryTest.php @@ -44,7 +44,7 @@ public function testCreateServiceWithNoTranslatorPluginManagerDefined(): void ->willReturn(false); $serviceLocator->expects(self::once()) - ->method('get') + ->method('get') ->with('config') ->willReturn([]); From 58bf69405701b52dea179f210b3024ee0ea236a9 Mon Sep 17 00:00:00 2001 From: Kevin Hamilton Date: Fri, 24 May 2024 09:24:16 +0100 Subject: [PATCH 18/18] corrections for phpcs Signed-off-by: Kevin Hamilton --- src/ConfigProvider.php | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 188f92ef..7ac87880 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -161,23 +161,23 @@ public function getViewHelperConfig() { return [ 'aliases' => [ - 'countryCodeDataList' => View\Helper\CountryCodeDataList::class, - 'currencyformat' => View\Helper\CurrencyFormat::class, - 'currencyFormat' => View\Helper\CurrencyFormat::class, - 'CurrencyFormat' => View\Helper\CurrencyFormat::class, - 'dateformat' => View\Helper\DateFormat::class, - 'dateFormat' => View\Helper\DateFormat::class, - 'DateFormat' => View\Helper\DateFormat::class, - 'numberformat' => View\Helper\NumberFormat::class, - 'numberFormat' => View\Helper\NumberFormat::class, - 'NumberFormat' => View\Helper\NumberFormat::class, - 'plural' => View\Helper\Plural::class, - 'Plural' => View\Helper\Plural::class, - 'translate' => View\Helper\TranslateWithParams::class, - 'Translate' => View\Helper\TranslateWithParams::class, - 'translateplural' => View\Helper\TranslatePluralWithParams::class, - 'translatePlural' => View\Helper\TranslatePluralWithParams::class, - 'TranslatePlural' => View\Helper\TranslatePluralWithParams::class, + 'countryCodeDataList' => View\Helper\CountryCodeDataList::class, + 'currencyformat' => View\Helper\CurrencyFormat::class, + 'currencyFormat' => View\Helper\CurrencyFormat::class, + 'CurrencyFormat' => View\Helper\CurrencyFormat::class, + 'dateformat' => View\Helper\DateFormat::class, + 'dateFormat' => View\Helper\DateFormat::class, + 'DateFormat' => View\Helper\DateFormat::class, + 'numberformat' => View\Helper\NumberFormat::class, + 'numberFormat' => View\Helper\NumberFormat::class, + 'NumberFormat' => View\Helper\NumberFormat::class, + 'plural' => View\Helper\Plural::class, + 'Plural' => View\Helper\Plural::class, + 'translate' => View\Helper\TranslateWithParams::class, + 'Translate' => View\Helper\TranslateWithParams::class, + 'translateplural' => View\Helper\TranslatePluralWithParams::class, + 'translatePlural' => View\Helper\TranslatePluralWithParams::class, + 'TranslatePlural' => View\Helper\TranslatePluralWithParams::class, // Legacy Zend Framework aliases 'Zend\I18n\View\Helper\CurrencyFormat' => View\Helper\CurrencyFormat::class,