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