Skip to content

Commit

Permalink
Implement localization support in Progressive Web App bundle (#182)
Browse files Browse the repository at this point in the history
Updated various modules to support localization, primarily adjusting how manifest files are generated and served. Added new "locales" configuration parameter, made necessary adjustments in manifest and service worker compilers. Additionally, refactored the twig runtime to properly handle localized manifest URLs.
  • Loading branch information
Spomky authored Apr 23, 2024
1 parent 82ce386 commit 934f1dc
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 27 deletions.
5 changes: 5 additions & 0 deletions src/Dto/Manifest.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ final class Manifest
#[SerializedName('iarc_rating_id')]
public null|string $iarcRatingId = null;

/**
* @var array<string>
*/
public array $locales = [];

/**
* @var array<ScopeExtension>
*/
Expand Down
7 changes: 5 additions & 2 deletions src/Dto/TranslatableTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
16 changes: 15 additions & 1 deletion src/Resources/config/definition/manifest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,31 @@
->children()
->arrayNode('manifest')
->canBeEnabled()
->validate()
->ifTrue(
static fn (array $v) => count(
$v['locales']
) !== 0 && ! str_contains((string) $v['public_url'], '{locale}')
)
->thenInvalid(
'When setting locales, the public URL "public_url" must contain the "{locale}" placeholder.'
)
->end()
->children()
->scalarNode('public_url')
->defaultValue('/site.webmanifest')
->cannotBeEmpty()
->info('The public URL of the manifest file.')
->example('/site.manifest')
->example(['/site.manifest', '/site.{locale}.webmanifest'])
->end()
->booleanNode('use_credentials')
->defaultTrue()
->info('Indicates whether the manifest should be fetched with credentials.')
->end()
->arrayNode('locales')
->defaultValue([])
->scalarPrototype()->end()
->end()
->scalarNode('background_color')
->info(
'The background color of the application. It should match the background-color CSS property in the sites stylesheet for a smooth transition between launching the web application and loading the site\'s content.'
Expand Down
38 changes: 32 additions & 6 deletions src/Subscriber/ManifestCompileEventListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,14 +43,14 @@ public function __construct(
private PublicAssetsFilesystemInterface $assetsFilesystem,
#[Autowire('%kernel.debug%')]
bool $debug,
null|EventDispatcherInterface $dispatcher = null,
null|EventDispatcherInterface $dispatcher,
) {
$this->dispatcher = $dispatcher ?? new NullEventDispatcher();
$this->manifestPublicUrl = '/' . trim($manifestPublicUrl, '/');
$options = [
AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES => true,
AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
AbstractNormalizer::IGNORED_ATTRIBUTES => ['useCredentials'],
AbstractNormalizer::IGNORED_ATTRIBUTES => ['useCredentials', 'locales'],
JsonEncode::OPTIONS => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR,
];
if ($debug === true) {
Expand All @@ -62,10 +65,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->manifest->locales) === 0) {
$this->compileManifest($manifest, null);
} else {
foreach ($this->manifest->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);
}
}
46 changes: 33 additions & 13 deletions src/Subscriber/PwaDevServerSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -51,13 +53,13 @@ public function __construct(
private ServiceWorkerCompiler $serviceWorkerBuilder,
private SerializerInterface $serializer,
private Manifest $manifest,
private ServiceWorker $serviceWorker,
ServiceWorker $serviceWorker,
#[Autowire('%spomky_labs_pwa.manifest.public_url%')]
string $manifestPublicUrl,
private null|Profiler $profiler,
#[Autowire('%kernel.debug%')]
bool $debug,
null|EventDispatcherInterface $dispatcher = null,
null|EventDispatcherInterface $dispatcher,
) {
$this->dispatcher = $dispatcher ?? new NullEventDispatcher();
$this->manifestPublicUrl = '/' . trim($manifestPublicUrl, '/');
Expand All @@ -74,7 +76,7 @@ public function __construct(
$options = [
AbstractObjectNormalizer::SKIP_UNINITIALIZED_VALUES => true,
AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
AbstractNormalizer::IGNORED_ATTRIBUTES => ['useCredentials'],
AbstractNormalizer::IGNORED_ATTRIBUTES => ['useCredentials', 'locales'],
JsonEncode::OPTIONS => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR,
];
if ($debug === true) {
Expand All @@ -89,17 +91,28 @@ public function onKernelRequest(RequestEvent $event): void
return;
}

$pathInfo = $event->getRequest()
->getPathInfo();
$request = $event->getRequest();
$pathInfo = $request->getPathInfo();
$localizedManifestPublicUrls = [];
foreach ($this->manifest->locales as $locale) {
$localizedManifestPublicUrls[$locale] = str_replace('{locale}', $locale, $this->manifestPublicUrl);
}

switch (true) {
case $this->manifest->enabled === true && $pathInfo === $this->manifestPublicUrl:
case $this->manifest->enabled === 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->manifest->enabled === true && count(
$localizedManifestPublicUrls
) === 0 && $pathInfo === $this->manifestPublicUrl:
$this->serveManifest($event);
break;
case $this->serviceWorker->enabled === true && $pathInfo === $this->serviceWorkerPublicUrl:
case $this->manifest->serviceWorker?->enabled === true && $pathInfo === $this->serviceWorkerPublicUrl:
$this->serveServiceWorker($event);
break;
case $this->serviceWorker->enabled === true && $this->workboxVersion !== null && $this->workboxPublicUrl !== null && str_starts_with(
case $this->manifest->serviceWorker?->enabled === true && $this->workboxVersion !== null && $this->workboxPublicUrl !== null && str_starts_with(
$pathInfo,
$this->workboxPublicUrl
):
Expand Down Expand Up @@ -129,16 +142,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,
Expand Down
20 changes: 15 additions & 5 deletions src/Twig/PwaRuntime.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,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->manifest->enabled === true) {
$output = $this->injectManifestFile($output);
$output = $this->injectManifestFile($output, $locale);
}
if ($this->manifest->serviceWorker?->enabled === true) {
$output = $this->injectServiceWorker($output, $injectSW, $swAttributes);
Expand All @@ -49,15 +50,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<link rel="manifest"%s href="%s">', PHP_EOL, $useCredentials, $url);
return $output . sprintf('%s<link rel="manifest" href="%s"%s%s>', PHP_EOL, $url, $useCredentials, $hreflang);
}

private function injectThemeColor(string $output, bool $themeColor): string
Expand Down

0 comments on commit 934f1dc

Please sign in to comment.