Este foro ya no está activo, así que no puedes publicar nuevas preguntas ni responder a las preguntas existentes.

Traducción de base de datos, symfony 2.4

3 de julio de 2014

Hola, necesito hacer una web que esté en dos idiomas (es, en) y no consigo hacer la traducción con la base de datos.

Estoy usando l3pp4rd/DoctrineExtensions que me funciona, pero lo que no consigo hacer es lo de un formulario con los campos para traducir.

He estado mirando y está esto: a2lix/TranslationFormBundle, pero no conseguí que funcionase. Me mostraba unos campos pero no los de la tabla sino los que generaba el para el tratamiento de la traducción.

Al no ir opté por qrizly/2437078 como recomienda el bundle de l3pp4rd, aunque he tenido que actualizar varias cosas por el tema de que está hecho para la versión 2.1 de formularios y yo estoy con symfony 2.4. Aún así, no consigo que me muestre la caja de texto de forma adecuada. Pongo el código para que lo veáis y lo que genera Twig al final del todo.

PD: lo pongo en varios porque está limitado el texto a 5000 bytes

El formulario:

// ...
 
class CorrespondenciaType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('titulo')
            ->add('direccion')
            ->add('telefono')
            ->add('email', 'email')
            ->add('precioVisitantes')
            ->add('precioClub')
            ->add('imagen', 'hidden')
            ->add('foto', 'file', array(
                'mapped' => false,
                'required' => false
            ))
            ->add('titulo_ingles', 'translated_field', array(
                'field'          => 'titulo',
                'property_path'  => 'translations',
                'locales'        => array('en'),
                'personal_translation' => 'Escuela\BackendBundle\Entity\CorrespondenciaTranslation'
            ))
        ;
    }
 
    // ...
}

TranslatedFieldType:

namespace Escuela\BackendBundle\Form\Type;
 
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
 
use Escuela\BackendBundle\Form\EventListener\AddTranslatedFieldSubscriber;
 
/**
 * Class TranslatedFieldType
 * @package Escuela\BackendBundle\Form\Type
 */
class TranslatedFieldType extends AbstractType
{
    protected $container;
 
    /**
     * @param ContainerInterface $container
     */
    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }
 
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     * @throws \InvalidArgumentException
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        if(!class_exists($options['personal_translation']))
        {
            Throw new \InvalidArgumentException(sprintf("Unable to find personal translation class: '%s'", $options['personal_translation']));
        }
        if(! $options['field'])
        {
            Throw new \InvalidArgumentException("You should provide a field to translate");
        }
 
        $subscriber = new AddTranslatedFieldSubscriber($builder->getFormFactory(), $this->container, $options);
        $builder->addEventSubscriber($subscriber);
    }
 
    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'remove_empty' => true,
            'csrf_protection' => false,
            'personal_translation' => false, //Personal Translation class
            'locales' => array('es', 'en'), //the locales you wish to edit
            'required_locale' => array('en'), //the required locales cannot be blank
            'field' => false, //the field that you wish to translate
            'widget' => "text", //change this to another widget like 'texarea' if needed
            'entity_manager_removal' => true, //aut
        ));
 
    }
 
    public function getName()
    {
        return 'translated_field';
    }
}

Respuestas

#1

AddTranslatedFieldSubscriber:

namespace Escuela\BackendBundle\Form\EventListener;
 
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormFactoryInterface;
 
/**
* Class AddTranslatedFieldSubscriber
* @package ExampleBundle\Form\EventListener
*/
class AddTranslatedFieldSubscriber implements EventSubscriberInterface
{
  private $factory;
  private $options;
  private $container;
  /**
  * @param FormFactoryInterface $factory
  * @param ContainerInterface $container
  * @param array $options
  */
  public function __construct(FormFactoryInterface $factory, ContainerInterface $container, Array $options)
  {
    $this->factory = $factory;
    $this->options = $options;
    $this->container = $container;
  }
 
  public static function getSubscribedEvents()
  {
    // Tells the dispatcher that we want to listen on the form.pre_set_data
    // , form.post_data and form.bind_norm_data event
    return array(
      FormEvents::PRE_SET_DATA => 'preSetData',
      FormEvents::POST_SUBMIT => 'postSubmit',
      FormEvents::SUBMIT => 'submit'
    );
  }
 
  private function bindTranslations($data)
  {
    //Small helper function to extract all Personal Translation
    //from the Entity for the field we are interested in
    //and combines it with the fields
 
    $collection = array();
    $availableTranslations = array();
 
    foreach($data as $Translation){
      if(strtolower($Translation->getField()) == strtolower($this->options['field'])){
        $availableTranslations[ strtolower($Translation->getLocale()) ] = $Translation;
      }
    }
 
    foreach($this->getFieldNames() as $locale => $fieldName){
      if(isset($availableTranslations[ strtolower($locale) ])){
        $Translation = $availableTranslations[ strtolower($locale) ];
      }
      else{
        $Translation = $this->createPersonalTranslation($locale, $this->options['field'], NULL);
      }
 
      $collection[] = array(
        'locale'      => $locale,
        'fieldName'   => $fieldName,
        'translation' => $Translation,
      );
    }
 
    return $collection;
  }
 
  private function getFieldNames(){
    //helper function to generate all field names in format:
    // '<locale>' => '<field>:<locale>'
    $collection = array();
 
    foreach($this->options['locales'] as $locale){
      $collection[ $locale ] = $this->options['field'] .":". $locale;
    }
 
    return $collection;
  }
 
  private function createPersonalTranslation($locale, $field, $content){
    //creates a new Personal Translation
    $className = $this->options['personal_translation'];
 
    $Translation = new $className($locale, $field, $content);
 
    return $Translation;
  }
 
  public function submit(FormEvent $event){
    //Validates the submitted form
    $data = $event->getData();
    $form = $event->getForm();
 
    $validator = $this->container->get('validator');
 
    foreach($this->getFieldNames() as $locale => $fieldName){
      $content = $form->get($fieldName)->getData();
 
      if(NULL === $content && in_array($locale, $this->options['required_locale'])){
        $form->addError(new FormError(sprintf("Field '%s' for locale '%s' cannot be blank", $this->options['field'], $locale)));
      }else{
        $Translation = $this->createPersonalTranslation($locale, $fieldName, $content);
        $errors = $validator->validate($Translation, array(sprintf("%s:%s", $this->options['field'], $locale)));
 
        if(count($errors) > 0){
          foreach($errors as $error){
           $form->addError(new FormError($error->getMessage()));
          }
        }
      }
    }
  }
 
  public function postSubmit(FormEvent $event){
    //if the form passed the validattion then set the corresponding Personal Translations
    $form = $event->getForm();
    $data = $form->getData();
 
    $entity = $form->getParent()->getData();
 
    foreach($this->bindTranslations($data) as $binded){
      $content = $form->get($binded['fieldName'])->getData();
      $Translation = $binded['translation'];
 
      // set the submitted content
      $Translation->setContent($content);
 
      //test if its new
      if($Translation->getId()){
        //Delete the Personal Translation if its empty
        if(NULL === $content && $this->options['remove_empty']){
            $data->removeElement($Translation);
 
          if($this->options['entity_manager_removal']){
            $this->container->get('doctrine.orm.entity_manager')->remove($Translation);
          }
        }
      }elseif(NULL !== $content){
        //add it to entity
        $entity->addTranslation($Translation);
 
        if(! $data->contains($Translation)){
          $data->add($Translation);
        }
      }
    }
  }
 
  public function preSetData(FormEvent $event){
    //Builds the custom 'form' based on the provided locales
    $data = $event->getData();
    $form = $event->getForm();
 
    // During form creation setData() is called with null as an argument
    // by the FormBuilder constructor. We're only concerned with when
    // setData is called with an actual Entity object in it (whether new,
    // or fetched with Doctrine). This if statement let's us skip right
    // over the null condition.
    if (null === $data){
      return;
    }
 
    foreach($this->bindTranslations($data) as $binded){
      $form->add($this->factory->createNamed(
        $binded['fieldName'],
        $this->options['widget'],
        $binded['translation']->getContent(),
        array(
          'label' => $binded['locale'],
          'required' => in_array($binded['locale'], $this->options['required_locale']),
          'mapped'=> false,
          'auto_initialize' => false
        )
      ));
    }
  }
}

@AlbertoVioque

3 julio 2014, 20:45
#2

Services:

services:
        form.type.translatable:
        class: Escuela\BackendBundle\Form\Type\TranslatedFieldType
        arguments: [ @service_container ]
        tags:
            - { name: form.type, alias: translated_field }

La vista:

{% block contenido %}
...
  {% form_theme edit_form with 'BootstrapBundle:Default/Backend/Form:fields.html.twig' %}
     {{ form_start(edit_form, {'attr': {'role': 'form'} }) }}
         ...
         {{ form_row(edit_form.titulo_ingles) }}
         ...
     {{ form_end(edit_form) }}
     ...
 {% endblock %}

Lo que ocurre es como si intentase mostrar dos veces el campo titulo_ingles

HTML Generado:

<div class="form-group">
    <label class="required">Titulo ingles</label>
    <div id="escuela_backendbundle_correspondencia_titulo_ingles" class="form-control">
        <div class="form-group">
            <label for="escuela_backendbundle_correspondencia_titulo_ingles_titulo:en" class="required">en</label>
            <input type="text" id="escuela_backendbundle_correspondencia_titulo_ingles_titulo:en" name="escuela_backendbundle_correspondencia[titulo_ingles][titulo:en]" required="required" class="form-control">
        </div>
    </div>
</div>

A ver si sabéis que ocurre o si se os ocurre otra forma más sencilla, fácil y rápida. Gracias!!!

@AlbertoVioque

3 julio 2014, 20:46
#3

Aunque nunca he usado nunca la extensión translatable de Doctrine en aplicaciones reales, sí que veo una cosa rara en el código que has compartido. En el formulario estás añadiendo un campo llamado titulo y otro titulo_ingles:

->add('titulo')
// ...
->add('titulo_ingles', 'translated_field', array(
    'field'          => 'titulo',
    'property_path'  => 'translations',
    'locales'        => array('en'),
    'personal_translation' => 'Escuela\BackendBundle\Entity\CorrespondenciaTranslation'
))

Creo que la gracia de estas extensiones es que tu defines un solo campo (titulo) y le dices en cuántos idiomas está disponible. Luego el ya se encarga de guardar la información correspondiente para cada traducción de cada campo de formulario. No se si este código funciona, pero debería ser algo así:

// ...
->add('titulo', 'translated_field', array(
    'field'          => 'titulo',
    'property_path'  => 'translations',
    'locales'        => array('es', 'en'),
    'personal_translation' => 'Escuela\BackendBundle\Entity\CorrespondenciaTranslation'
))

@javiereguiluz

4 julio 2014, 12:46
#4

Hola Javier, la extensión translatable lo que hace es gestionar la traducción en la base de datos, en la forma de guardarlos, pero no hace nada con los formularios, tienes que crear el formulario para traducir y luego tratar los datos para guardarlo con otro idioma. El bundle que te he puesto: enlace si hace eso, pero trabaja con otros bundles como el de doctrineExtensions, el de a2lix se encarga de crear un formulario con las distintas traducciones que tu le dices y doctrineExtensions se encarga de guardarlo, lo que pasa que no conseguí hacerlo funcionar, en vez de crearme los campos que quería traducir en la vista, este creaba los campos de la base de datos que se encarga de gestionar la traducción, por eso lo deseche.

Lo que quiero es un formulario que tenga la información en español y luego los campos que necesito para traducir en ingles, todo en la misma vista, pero poder elegir donde va a salir estos campos.

Sobre mi código, no sé si lo estoy haciendo del todo bien, lo que pasa es que me pone dos veces el label, el guardar en la base de datos lo hace bien, he probado a quitar el TranslatedFieldTypem y añadir el AddTranslatedFieldSubscriber directamente en el form, y así no me muestra repetido el label en la vista pero de la otra manera no hay manera, es como si me añadiese el label por el TranslatedFielType y otro por el campo añadido en el Subscriber, en la vista si pongo solo el:

{{ form_row(edit_form.titulo_ingles) }}

Me imprime dos label y un input, si pongo

{{ form_widget(edit_form.titulo_ingles) }}

Me imprime un label y un input, cuando solo debería crear un input

@AlbertoVioque

5 julio 2014, 17:37