Cambiar idioma/locale de la aplicación según las preferencias del usuario

Preferencias de idioma

Este post es la segunda parte del anterior que escribí relacionado con la internacionalización de nuestra aplicacion web y tratará de como traducir nuestra aplicación según el idioma que el usuario haya seleccionado en su configuración personal.

Para ello asumiremos que tenemos una entidad UserPreferences relacionada con nuestra entidad User con una relacion OneToOne, y otra entidad Language relacionada con UserPreferences con una relación ManyToOne, donde guardaremos los posibles “locales” de la aplicación, de la siguiente forma:

User:

/**
 * @ORM\OneToOne(targetEntity="efor\AppBundle\Entity\UserPreference", inversedBy="user", cascade={"persist"})
 */
protected $userPreference;

UserPreference:

/**
 * @ORM\Id
 * @ORM\Column(type="integer", nullable=false)
 * @ORM\GeneratedValue()
 */
private $id;

/**
 * @ORM\ManyToOne(targetEntity="efor\AppBundle\Entity\Language", inversedBy="userPreference", fetch="EAGER")
 */
private $language;

/**
 * @ORM\OneToOne(targetEntity="efor\UsuarioBundle\Entity\User", mappedBy="userPreference")
 */
private $user;

Language:

/**
 * @ORM\Id
 * @ORM\Column(type="integer", nullable=false)
 * @ORM\GeneratedValue()
 */
private $id;

/**
 * @ORM\Column(name="idioma_nombre", type="string", length=50, nullable=false)
 */
private $name;

/**
 * @ORM\Column(name="idioma_abreviatura", type="string", length=4, nullable=true)
 */
private $slug;

/**
 * @ORM\Column(name="idioma_codigo", type="integer", nullable=true)
 */
private $code;

/**
 * @ORM\Column(name="idioma_activo", type="boolean", nullable=false)
 */
private $active = true;

/**
 * @ORM\Column(name="idioma_borrado", type="boolean", nullable=false)
 */
private $deleted = false;

/**
 * @ORM\OneToMany(targetEntity="efor\AppBundle\Entity\UserPreference", mappedBy="language")
 */
private $userPreference;

Todas estas entidades van con sus getters/setters correspondientes.

Montamos el formulario para mostrar los idiomas disponibles que en nuestro caso los tendremos guardados mediante fixtures o migración de datos en la entidad Language. Quedaría así:

class UserPreference extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('language', EntityType::class,array(
                'label' => 'Idioma'
            ));
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'efor\AppBundle\Entity\UserPreference'
        ));
    }

    public function getName()
    {
        return 'usuario_bundle_user_preference';
    }
}

Y este formulario lo añadiremos a nuestro formulario de configuración del perfil del usuario, tal que así:

class ProfileType extends AbstractType
{
    /** @var ObjectManager $manager */
    private $manager;

    /** @var Language $lang */
    private $lang;

    public function __construct(ObjectManager $manager, Language $lang)
    {
        $this->manager = $manager;
        $this->lang = $lang;
    }

    /**
     * @param FormBuilderInterface $builder
     * @param array                $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', TextType::class, array('label' => 'name'))
            ->add('surnameFirst', TextType::class, array('label' => 'first_name'))
            ->add('surnameSecond', TextType::class, array('label' => 'last_name'))
            ->add('email', EmailType::class, array('label' => 'Email'))
            ->add('plainPassword', 'password', array(
                'label' => 'Nuevo Password',
                'required' => false,
            ))
            ->add('salt', HiddenType::class)
            ->add('photo', new UploadFileType(), array(
                'label' => 'Foto',
                'required' => false,
            ))
            ->add('city', TextType::class, array(
                'label' => 'city',
            ))
            ->add('postalCode', TextType::class, array(
                'label' => 'postal_code',
            ))
            ->add('country', TextType::class, array(
                'label' => 'country',
            ))
            ->add('birthdate', DateType::class, array(
                'label' => 'birthdate',
                'widget' => 'single_text',
                'format' => 'dd/MM/yyyy',
                'attr' => array(
                    'class' => 'form-control input-inline datepicker',
                    'data-provide' => 'datepicker',
                    'data-date-format' => 'dd/mm/yyyy',
                    ),
                )
            )
->add('language', EntityType::class, array(
                'class' => 'AppBundle:Language',
                'data' => $this->lang,
                'label' => 'Idioma',
                'mapped' => false,
                'required' => true,
                'multiple' => false,
                'expanded' => true
            ))
        ;
    }

    /**
     * @param OptionsResolver $resolver
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'efor\UsuarioBundle\Entity\User'
        ));
    }

    /**
     * @return string
     */
    public function getName()
    {
        return 'efor_usuariobundle_User';
    }
}

En nuestro caso, este formulario recibe un objeto Language que se encargara de hidratar el campo “no mapeado” de los idiomas que lo pasaremos desde nuestro Controller al formulario de la siguiente forma:

public function editProfileAction(Request $request)
{
    /** @var User $user */
    $user = $this->getUser();
    $em = $this->getDoctrine()->getManager();
    $langUser = $user->getUserPreference()->getLanguage();
    $editProfileForm = $this->createForm(new ProfileType($em, $langUser), $user, array(
        'action' => $this->generateUrl('profile_edit'),
        'method' => 'POST',
    ));

    $editProfileForm
        ->add('update', 'submit', array(
            'attr' => array(
                'class' => 'boton pull-right'
            ),
            'label' => 'update'
        ));

    $editProfileForm->handleRequest($request);
    if ($editProfileForm->isValid()) {
        try {
            // Si el usuario no ha cambiado el password, su valor es null después
            // de hacer el ->bindRequest(), por lo que hay que recuperar el valor original
            if (null == $user->getPlainPassword()) {
                $user->setPassword($passwordOriginalEncripted);
            }

            /** @var Language $language */
            $language = $editProfileForm->get('language')->getData();
            $userPreference = new UserPreference();
            $userPreference->setLanguage($language);
            $userPreference->setUser($user);
            $user->setUserPreference($userPreference);

            $userManager = $this->getUserManager();
            $userManager->updateUser($user);

            $this->addFlashMessage(CoreController::FLASH_TYPE_SUCCES

            return $this->redirect($this->generateUrl('profile_show', array(
                '_locale' => $language->getSlug()
            )));


        } catch (\Exception $e) {
            $editProfileForm->addError(new FormError($e->getMessage()));

            return $this->render('UsuarioBundle:Profile:editProfile.html.twig', array(
                'form' => $editProfileForm->createView(),
            ));
        }
    }

    return $this->render('UsuarioBundle:Profile:editProfile.html.twig', array(
        'form' => $editProfileForm->createView(),
        'form_errors_validation_document' => $editProfileForm->getErrorsAsString(),
    ));
}

Para que funcione el cambio del “locale” de la aplicación, tendremos que pasarle el parámetro “_locale” en la URL.

Siguiendo la receta del libro de Symfony, tendremos que crear un LocaleListener que se encargara de atender cada petición en el evento ‘onKernelRequest’ y comprobar si existe un parámetro que sea ‘_locale’ para cambiar en la session el locale y que la aplicacion se traduzca. Para ello, creamos una carpeta EventListener dentro de nuestro bundle, y dentro creamos la clase LocaleListener, tal que asi:

<?php
namespace efor\CoreBundle\EventListener;

use Stof\DoctrineExtensionsBundle\DependencyInjection\Compiler\SecurityContextPass;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;

class LocaleListener implements EventSubscriberInterface
{
    private $defaultLocale;

    /**
     * LocaleListener constructor.
     *
     * @param string $defaultLocale
     */
    public function __construct($defaultLocale = 'es')
    {
        $this->defaultLocale = $defaultLocale;
    }

    /**
     * @param GetResponseEvent $event
     */
    public function onKernelRequest(GetResponseEvent $event)
    {
        $request = $event->getRequest();
        if (!$request->hasPreviousSession()) {
            return;
        }

        // try to see if the locale has been set as a _locale routing parameter
        if ($locale = $request->attributes->get('_locale')) {
            $request->getSession()->set('_locale', $locale);
        } else {
            // if no explicit locale has been set on this request, use one from the session
            $request->setLocale($request->getSession()->get('_locale', $this->defaultLocale));
        }
    }

    public static function getSubscribedEvents()
    {
        return array(
            // must be registered after the default Locale listener
            KernelEvents::REQUEST => array(array('onKernelRequest', 150)),
        );
    }
}

Después añadimos el listener como un servicio dentro del archivo app/config/services.yml :

services:

    app.locale_listener:
        class: efor\CoreBundle\Listener\LocaleListener
        arguments: ['%kernel.default_locale%']
        tags:
            - { name: kernel.event_subscriber }

Ademas, si queremos que cuando el usuario haga login se cambie el idioma de la aplicación, según el idioma que tenga guardado en sus preferencias, debemos hacerlo en otro listener. En mi aplicación, dispongo de un listener que se ejecuta después del login satisfactorio del usuario y es allí donde busco el locale en las preferencias y lo envió.  La documentación para este listener la tenéis aquí.

<?php

namespace efor\UsuarioBundle\Redirection;

use efor\AppBundle\Entity\Role;
use efor\UsuarioBundle\Entity\User;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;

class AfterLoginRedirection implements AuthenticationSuccessHandlerInterface
{
    const DEFAULT_LOCALE = 'es';

    /** @var RouterInterface */
    private $router;

    /**
     * AfterLoginRedirection constructor.
     *
     * @param RouterInterface $router
     */
    public function __construct(RouterInterface $router)
    {
        $this->router = $router;
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token)
    {
        //Default redirect
        $response = new RedirectResponse($this->router->generate('fos_user_security_login'));

        if ($token->isAuthenticated()) {
                //Go home with user locale preferences
                /** @var User $user */
                $user = $token->getUser();
                $userPreference = $user->getUserPreference();
                $lang = $userPreference ? $userPreference->getLanguage()->getSlug() : self::DEFAULT_LOCALE;

                $response = new RedirectResponse($this->router->generate('dashboard_homepage', array(
                    '_locale' => $lang
                )));
        }
        // Go login
        return $response;
    }
}

Y tenemos este listener definido como servicio dentro de nuestro bundle, tal que así:

<service id="redirect.after.login" class="efor\UsuarioBundle\Redirection\AfterLoginRedirection">
    <argument type="service" id="router" />
</service>

En resumen, en mi caso solo cambiando el locale en la session, request y translator, no me funcionaba, por lo que he tenido que recurrir a la alternativa de pasar el locale por GET en la url y que el listener fuera el que hiciera el trabajo sucio.

No se si sera mejor o peor forma de hacerlo, el caso es que funciona y muy bien.

Si alguien tiene dudas, o quiere comentar algo, no dudéis en escribirme y lo comentamos.

Por favor, comparte si te a sido útil.

Hasta la vista SymfonyDevs ¡

 

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s