diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index b640631..0e73f87 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -665,6 +665,11 @@ parameters: count: 3 path: src/Resources/config/definition/service_worker.php + - + message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeDefinition\\:\\:children\\(\\)\\.$#" + count: 1 + path: src/Resources/config/definition/translation.php + - message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeParentInterface\\:\\:end\\(\\)\\.$#" count: 1 diff --git a/src/Dto/TranslatableTrait.php b/src/Dto/TranslatableTrait.php index a4abf34..5d67ddd 100644 --- a/src/Dto/TranslatableTrait.php +++ b/src/Dto/TranslatableTrait.php @@ -21,9 +21,12 @@ public function provideTranslation(null|string|array $data): null|string|Transla return $data; } if (is_array($data)) { - return array_map(fn (string $value): TranslatableInterface => new TranslatableMessage($value), $data); + return array_map( + fn (string $value): TranslatableInterface => new TranslatableMessage($value, [], 'pwa'), + $data + ); } - return new TranslatableMessage($data); + return new TranslatableMessage($data, [], 'pwa'); } } diff --git a/src/Resources/config/definition/translation.php b/src/Resources/config/definition/translation.php new file mode 100644 index 0000000..acb673e --- /dev/null +++ b/src/Resources/config/definition/translation.php @@ -0,0 +1,14 @@ +rootNode() + ->children() + ->arrayNode('locales') + ->scalarPrototype()->end() + ->end() + ->end(); +}; diff --git a/src/SpomkyLabsPwaBundle.php b/src/SpomkyLabsPwaBundle.php index 6dd5843..ef7571d 100644 --- a/src/SpomkyLabsPwaBundle.php +++ b/src/SpomkyLabsPwaBundle.php @@ -38,6 +38,7 @@ public function loadExtension(array $config, ContainerConfigurator $container, C $builder->setAlias('pwa.web_client', $config['web_client']); } $builder->setParameter('spomky_labs_pwa.screenshot_user_agent', $config['user_agent']); + $builder->setParameter('spomky_labs_pwa.locales', $config['locales']); $serviceWorkerConfig = $config['serviceworker']; $manifestConfig = $config['manifest']; diff --git a/src/Subscriber/ManifestCompileEventListener.php b/src/Subscriber/ManifestCompileEventListener.php index 65b89c9..fbf7d48 100644 --- a/src/Subscriber/ManifestCompileEventListener.php +++ b/src/Subscriber/ManifestCompileEventListener.php @@ -16,7 +16,10 @@ use Symfony\Component\Serializer\Encoder\JsonEncode; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; +use Symfony\Component\Serializer\Normalizer\TranslatableNormalizer; use Symfony\Component\Serializer\SerializerInterface; +use function assert; +use function count; use const JSON_PRETTY_PRINT; use const JSON_THROW_ON_ERROR; use const JSON_UNESCAPED_SLASHES; @@ -31,6 +34,9 @@ private array $jsonOptions; + /** + * @param string[] $locales + */ public function __construct( private SerializerInterface $serializer, private Manifest $manifest, @@ -42,7 +48,9 @@ public function __construct( private PublicAssetsFilesystemInterface $assetsFilesystem, #[Autowire('%kernel.debug%')] bool $debug, - null|EventDispatcherInterface $dispatcher = null, + null|EventDispatcherInterface $dispatcher, + #[Autowire('%spomky_labs_pwa.locales%')] + private array $locales, ) { $this->dispatcher = $dispatcher ?? new NullEventDispatcher(); $this->manifestPublicUrl = '/' . trim($manifestPublicUrl, '/'); @@ -64,10 +72,33 @@ public function __invoke(PreAssetsCompileEvent $event): void return; } $manifest = clone $this->manifest; - $this->dispatcher->dispatch(new PreManifestCompileEvent($manifest)); - $data = $this->serializer->serialize($manifest, 'json', $this->jsonOptions); + if (count($this->locales) === 0) { + $this->compileManifest($manifest, null); + } else { + foreach ($this->locales as $locale) { + $this->compileManifest($manifest, $locale); + } + } + } + + private function compileManifest(Manifest $manifest, null|string $locale): void + { + $preEvent = new PreManifestCompileEvent($manifest); + $preEvent = $this->dispatcher->dispatch($preEvent); + assert($preEvent instanceof PreManifestCompileEvent); + + $options = $this->jsonOptions; + $manifestPublicUrl = $this->manifestPublicUrl; + if ($locale !== null) { + $options[TranslatableNormalizer::NORMALIZATION_LOCALE_KEY] = $locale; + $manifestPublicUrl = str_replace('{locale}', $locale, $this->manifestPublicUrl); + } + $data = $this->serializer->serialize($preEvent->manifest, 'json', $options); + $postEvent = new PostManifestCompileEvent($manifest, $data); - $this->dispatcher->dispatch($postEvent); - $this->assetsFilesystem->write($this->manifestPublicUrl, $postEvent->data); + $postEvent = $this->dispatcher->dispatch($postEvent); + assert($postEvent instanceof PostManifestCompileEvent); + + $this->assetsFilesystem->write($manifestPublicUrl, $postEvent->data); } } diff --git a/src/Subscriber/PwaDevServerSubscriber.php b/src/Subscriber/PwaDevServerSubscriber.php index 0083454..f8f34e6 100644 --- a/src/Subscriber/PwaDevServerSubscriber.php +++ b/src/Subscriber/PwaDevServerSubscriber.php @@ -22,9 +22,11 @@ use Symfony\Component\Serializer\Encoder\JsonEncode; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; +use Symfony\Component\Serializer\Normalizer\TranslatableNormalizer; use Symfony\Component\Serializer\SerializerInterface; use function assert; use function count; +use function in_array; use function is_array; use function is_string; use function mb_strlen; @@ -47,6 +49,9 @@ private array $jsonOptions; + /** + * @param string[] $locales + */ public function __construct( private ServiceWorkerCompiler $serviceWorkerBuilder, private SerializerInterface $serializer, @@ -61,7 +66,9 @@ public function __construct( private null|Profiler $profiler, #[Autowire('%kernel.debug%')] bool $debug, - null|EventDispatcherInterface $dispatcher = null, + null|EventDispatcherInterface $dispatcher, + #[Autowire('%spomky_labs_pwa.locales%')] + private array $locales, ) { $this->dispatcher = $dispatcher ?? new NullEventDispatcher(); $this->manifestPublicUrl = '/' . trim($manifestPublicUrl, '/'); @@ -93,11 +100,22 @@ public function onKernelRequest(RequestEvent $event): void return; } - $pathInfo = $event->getRequest() - ->getPathInfo(); + $request = $event->getRequest(); + $pathInfo = $request->getPathInfo(); + $localizedManifestPublicUrls = []; + foreach ($this->locales as $locale) { + $localizedManifestPublicUrls[$locale] = str_replace('{locale}', $locale, $this->manifestPublicUrl); + } switch (true) { - case $this->manifestEnabled === true && $pathInfo === $this->manifestPublicUrl: + case $this->manifestEnabled === true && in_array($pathInfo, $localizedManifestPublicUrls, true): + $locale = array_search($pathInfo, $localizedManifestPublicUrls, true); + assert(is_string($locale), 'Locale not found.'); + $this->serveManifest($event, $locale); + break; + case $this->manifestEnabled === true && count( + $localizedManifestPublicUrls + ) === 0 && $pathInfo === $this->manifestPublicUrl: $this->serveManifest($event); break; case $this->serviceWorkerEnabled === true && $pathInfo === $this->serviceWorkerPublicUrl: @@ -133,16 +151,23 @@ public static function getSubscribedEvents(): array ]; } - private function serveManifest(RequestEvent $event): void + private function serveManifest(RequestEvent $event, null|string $locale = null): void { $this->profiler?->disable(); $manifest = clone $this->manifest; - $this->dispatcher->dispatch(new PreManifestCompileEvent($manifest)); - $data = $this->serializer->serialize($manifest, 'json', $this->jsonOptions); + $options = $this->jsonOptions; + if ($locale !== null) { + $options[TranslatableNormalizer::NORMALIZATION_LOCALE_KEY] = $locale; + } + $preEvent = new PreManifestCompileEvent($manifest); + $preEvent = $this->dispatcher->dispatch($preEvent); + assert($preEvent instanceof PreManifestCompileEvent); + $data = $this->serializer->serialize($preEvent->manifest, 'json', $options); $postEvent = new PostManifestCompileEvent($manifest, $data); - $this->dispatcher->dispatch($postEvent); + $postEvent = $this->dispatcher->dispatch($postEvent); + assert($postEvent instanceof PostManifestCompileEvent); - $response = new Response($data, Response::HTTP_OK, [ + $response = new Response($postEvent->data, Response::HTTP_OK, [ 'Cache-Control' => 'public, max-age=604800, immutable', 'Content-Type' => 'application/manifest+json', 'X-Manifest-Dev' => true, diff --git a/src/Twig/PwaRuntime.php b/src/Twig/PwaRuntime.php index e3ca916..e294aa7 100644 --- a/src/Twig/PwaRuntime.php +++ b/src/Twig/PwaRuntime.php @@ -39,11 +39,12 @@ public function load( bool $injectThemeColor = true, bool $injectIcons = true, bool $injectSW = true, - array $swAttributes = [] + array $swAttributes = [], + null|string $locale = null, ): string { $output = ''; if ($this->manifestEnabled === true) { - $output = $this->injectManifestFile($output); + $output = $this->injectManifestFile($output, $locale); } if ($this->serviceWorkerEnabled === true) { $output = $this->injectServiceWorker($output, $injectSW, $swAttributes); @@ -53,15 +54,24 @@ public function load( return $this->injectThemeColor($output, $injectThemeColor); } - private function injectManifestFile(string $output): string + private function injectManifestFile(string $output, null|string $locale): string { - $url = $this->assetMapper->getPublicPath($this->manifestPublicUrl) ?? $this->manifestPublicUrl; + $manifestPublicUrl = $locale === null ? $this->manifestPublicUrl : str_replace( + '{locale}', + $locale, + $this->manifestPublicUrl + ); + $url = $this->assetMapper->getPublicPath($manifestPublicUrl) ?? $manifestPublicUrl; $useCredentials = ''; if ($this->manifest->useCredentials === true) { $useCredentials = ' crossorigin="use-credentials"'; } + $hreflang = ''; + if ($locale !== null) { + $hreflang = sprintf(' hreflang="%s"', mb_strtolower(str_replace('_', '-', $locale))); + } - return $output . sprintf('%s', PHP_EOL, $useCredentials, $url); + return $output . sprintf('%s', PHP_EOL, $url, $useCredentials, $hreflang); } private function injectThemeColor(string $output, bool $themeColor): string