Rendre inaccessible des pages non traduites avec Drupal 8

Mappemonde

Il n'est pas rare lors de la réalisation d'un projet Drupal 8 multilingue, que les traductions des pages ne soient au rendez-vous lors de la mise en production. Autant la gestion du multilinguisme avec Drupal 8 est devenue réellement aisée à mettre en place, il y a désormais une différence fondamentale avec Drupal 7 : il est impossible de désactiver une langue. Soit on installe une langue, soit on la supprime. Cette modification est assumée, et volontaire, ceci afin de ne pas générer une complexité inutile pour une gain discutable.

Mais que faire, après avoir configuré, traduit les interfaces et soigné tout un projet multilingue, quand arrive la mise en production et qu'aucun contenu n'est traduit ?

Supprimer les langues non traduites ? Et perdre ainsi toute la traduction de la configuration du projet ? C'est une option.

Laisser les pages accessibles non traduites ? Cela peut donner une réelle mauvaise image du site. Qu'est-ce donc ce site qui propose plusieurs langues mais qui n'est pas traduit. Sans parler des url système qui seront immanquablement présentes, ni des impacts SEO (même si on peut les réduire significativement en ajoutant sur ces pages la balise meta noindex). Et plus généralement un aspect non fini. Et ne cherchez pas à cacher des urls, elles seront immanquablement trouvées.

Il existe une troisième alternative, plus douce. Laisser en place les langues installées sur le site, et tout simplement empêcher les visiteurs d'y accéder, en procédant à une redirection vers la page de la langue par défaut. Tout en permettant aux éditeurs de pouvoir accéder à ces pages et les traduire. Cela peut permettre aussi de donner du temps aux gestionnaires de contenu de traduire tout un site, pendant que celui-ci est en ligne.

Regardons comment implémenter cette solution.

Nous allons créer un EventSubscriber dans un module personnalisé my_module. Nous déclarons dans le fichier my_module.services.yml ce service que nous appelons my_module.language_access.

my_module.language_access:
  class: Drupal\my_module\EventSubscriber\LanguageAccessSubscriber
  arguments: ['@current_user', '@language_manager']
  tags:
    - { name: event_subscriber }

 

Notons que nous chargeons comme arguments à ce service, le service nous permettant de charger l'utilisateur courant (@current_user), et le service gérant les langues du site (@language_manager).

Nous avons déclaré notre class LanguageAccessSubscriber. Regardons tout de suite le code.

<?php

namespace Drupal\my_module\EventSubscriber;

use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Drupal\Core\Routing\RouteMatch;
use Symfony\Component\HttpFoundation\RedirectResponse;

/**
 * Class Subscriber.
 *
 * @package Drupal\my_module
 */
class LanguageAccessSubscriber implements EventSubscriberInterface {

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

  /**
   * The language manager.
   *
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

  /**
   * Constructor.
   *
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user.
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   the language manager.
   */
  public function __construct(AccountInterface $current_user, LanguageManagerInterface $language_manager) {
    $this->currentUser = $current_user;
    $this->languageManager = $language_manager;
  }

  /**
   * {@inheritdoc}
   */
  static function getSubscribedEvents() {
    $events[KernelEvents::REQUEST] = ['onRequestRedirect'];
    return $events;
  }

  /**
   * This method is called whenever the kernel.request event is
   * dispatched.
   *
   * @param GetResponseEvent $event
   */
  public function onRequestRedirect(GetResponseEvent $event) {
    $request = $event->getRequest();

    if ($this->currentUser->hasPermission('access non default language pages')) {
      return;
    }

    // If we've got an exception, nothing to do here.
    if ($request->get('exception') != NULL) {
      return;
    }

    $default_language = $this->languageManager->getDefaultLanguage();
    $active_language = $this->languageManager->getCurrentLanguage();

    if ($active_language->getId() != $default_language->getId()) {
      $route_match = RouteMatch::createFromRequest($request);
      $route_name = $route_match->getRouteName();
      $parameters = $route_match->getRawParameters()->all();
      $url = Url::fromRoute($route_name, $parameters, ['language' => $default_language]);
      $new_response = new RedirectResponse($url->toString(), '302');
      $event->setResponse($new_response);
    }
  }

}

Ce service déclaré comme un Eventsubscriber (cf le tags dans la déclaration du service) va souscrire à l'événement kernel.request, événement déclenché très tôt lors de la propagation d'une requête. Et à chaque requête, ce service va exécuter la méthode onRequestRedirect().

Si l'utilisateur courant dispose de la permission (à créer dans votre module) d'accéder à toutes les langues, alors nous ne faisons rien.

if ($this->currentUser->hasPermission('access non default language pages')) {
  return;
}

Puis après avoir récupéré à la fois la langue active et la langue par défaut, si alors celles-ci diffèrent, nous procédons à une redirection temporaire (302) vers la page demandée mais dans la version de la langue par défaut du site. Pour ce, nous créons un objet RouteMatch depuis la présente Request symfony, et générons une URL depuis cette route en lui passant comme paramètre supplémentaire la langue du site par défaut.

$url = Url::fromRoute($route_name, $parameters, ['language' => $default_language]);

Et il ne reste plus qu'à rediriger vers cette nouvelle url.

$new_response = new RedirectResponse($url->toString(), '302');
$event->setResponse($new_response);

Bien sûr vous pouvez tout aussi bien décider de rediriger vers une autre page, ou générer une page 403 ou 404. C'est selon vos besoins ou vos critères.

En quelques lignes, nous pouvons maintenant conserver toutes les langues installées sur le projet Drupal 8, donner de temps au temps pour les traduire correctement, et ceci sans impacter l'image ou le référencement du site.

Notons pour conclure, qu'il n'est pas inutile d'exclure ces pages non traduites de vos fichiers sitemap.xml si vous en générez un. Inutile d'indiquer aux moteurs de recherche l'existence de pages à indexer et pour lesquelles ils seront renvoyés vers leur équivalent dans la langue par défaut du site.

Une redirection page par page ?

Et si nous avions besoin de donner accès aux pages de contenu, dès que celles-ci sont traduites. Cela relève du même principe. Il suffirait alors de récupérer dans les paramètres de la route l'entité en question (par exemple les noeuds), vérifier que l'entité dispose bien d'une traduction dans la langue demandée, avec la méthode hasTranslation(), et sinon procéder à nouveau à une redirection. Toutes les stratégies d'attente sont possibles ici. 

Un module ?

N'y aurait-il pas ici un espace pour un module contribué ? Un développeur Drupal 8 motivé ?

 

Ajouter un commentaire