diff --git a/packages/drupal/custom/custom.services.yml b/packages/drupal/custom/custom.services.yml index 5adbb9068..7424d6f1d 100644 --- a/packages/drupal/custom/custom.services.yml +++ b/packages/drupal/custom/custom.services.yml @@ -10,3 +10,9 @@ services: custom.menus: class: Drupal\custom\Menus + + custom.entity_language_redirect_subscriber: + class: Drupal\custom\EventSubscriber\EntityLanguageRedirectSubscriber + arguments: ['@language_manager', '@current_route_match'] + tags: + - { name: event_subscriber } diff --git a/packages/drupal/custom/src/EventSubscriber/EntityLanguageRedirectSubscriber.php b/packages/drupal/custom/src/EventSubscriber/EntityLanguageRedirectSubscriber.php new file mode 100644 index 000000000..63016e2af --- /dev/null +++ b/packages/drupal/custom/src/EventSubscriber/EntityLanguageRedirectSubscriber.php @@ -0,0 +1,72 @@ +languageManager = $language_manager; + $this->routeMatch = $route_match; + } + + public static function getSubscribedEvents() { + // We need this subscriber to run after the router_listener service (which + // has priority 32) so that the parameters are set into the request, but + // before the EntityCanonicalViewSubscriber one (with the priority 28). So, + // we set the priority to 30. + return [ + KernelEvents::REQUEST => [ + ['onKernelRequest', 30], + ] + ]; + } + + public function onKernelRequest(RequestEvent $event): void { + // In case the user tries to access a node in a language entity is not + // translated to, we redirect to the entity in the original language and + // display a warning message. + if ($this->routeMatch->getRouteName() === 'entity.node.canonical') { + $entity = $this->routeMatch->getCurrentRouteMatch()->getParameter('node'); + $requestedLanguageId = $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(); + if ($entity->language()->getId() != $requestedLanguageId) { + $routeOptions = [ + 'language' => $entity->language(), + ]; + $routeParameters = [ + 'node' => $entity->id(), + ]; + $url = Url::fromRoute('entity.node.canonical', $routeParameters, $routeOptions); + // Make sure we keep any query strings. + $queryString = (string) $event->getRequest()->getQueryString(); + if ($queryString !== '') { + $queryString .= '&'; + } + $urlString = $url->toString() . '?' . $queryString . 'content_language_not_available=true&requested_language=' . $requestedLanguageId; + + // Add the necessary cache contexts to the response, as redirect + // responses are cached as well. + $metadata = new CacheableMetadata(); + $metadata->addCacheContexts(['languages:language_interface', 'url.query_args']); + $response = new TrustedRedirectResponse($urlString); + $response->addCacheableDependency($entity); + $response->addCacheableDependency($metadata); + + $event->setResponse($response); + } + } + } +} diff --git a/packages/ui/src/components/Molecules/Messages.tsx b/packages/ui/src/components/Molecules/Messages.tsx index b6563150a..c733a5b42 100644 --- a/packages/ui/src/components/Molecules/Messages.tsx +++ b/packages/ui/src/components/Molecules/Messages.tsx @@ -1,10 +1,18 @@ import { Html, Markup } from '@custom/schema'; -import React from 'react'; +import React, { ReactNode } from 'react'; // TODO: Style, add stories. -export function Messages(props: { messages: Array }) { - return
{buildMessages(props.messages)}
; +export function Messages(props: { + messages: Array; + messageComponents?: Array; +}) { + return ( +
+ {buildMessages(props.messages)} + {props.messageComponents} +
+ ); } export const buildMessages = (messages: Array) => ( diff --git a/packages/ui/src/components/Molecules/PageTransition.tsx b/packages/ui/src/components/Molecules/PageTransition.tsx index 72ca7569b..5b52b4bd8 100644 --- a/packages/ui/src/components/Molecules/PageTransition.tsx +++ b/packages/ui/src/components/Molecules/PageTransition.tsx @@ -1,16 +1,27 @@ +import { Locale } from '@custom/schema'; import { motion, useReducedMotion } from 'framer-motion'; -import React, { PropsWithChildren, useEffect } from 'react'; +import React, { PropsWithChildren, ReactNode, useEffect } from 'react'; import { Messages, readMessages } from './Messages'; export function PageTransition({ children }: PropsWithChildren) { const [messages, setMessages] = React.useState>([]); + const [messageComponents, setMessageComponents] = React.useState< + Array + >([]); useEffect(() => { + // Standard messages. setMessages(readMessages()); + // Language message. + const languageMessage = getLanguageMessage(window.location.href); + if (languageMessage) { + setMessageComponents([languageMessage]); + } }, []); + return useReducedMotion() ? (
- + {children}
) : ( @@ -26,8 +37,52 @@ export function PageTransition({ children }: PropsWithChildren) { duration: 0.3, }} > - + {children} ); } + +function getLanguageMessage(url: string): ReactNode { + const urlObject = new URL(url); + const contentLanguageNotAvailable = + urlObject.searchParams.get('content_language_not_available') === 'true'; + if (contentLanguageNotAvailable) { + const requestedLanguage = urlObject.searchParams.get('requested_language'); + if (requestedLanguage) { + const translations: { + [language in Locale]: { message: string; goBack: string }; + } = { + en: { + message: 'This page is not available in the requested language.', + goBack: 'Go back', + }, + de: { + message: + 'Diese Seite ist nicht in der angeforderten Sprache verfügbar.', + goBack: 'Zurück', + }, + }; + const translation = translations[requestedLanguage as Locale]; + if (translation) { + return ( + + ); + } else { + console.error( + `Requested language "${requestedLanguage}" not found in messages.`, + ); + } + } + } +}