I(blah…blah…blah)n

You love Doctrine2 and its explicit way of doing things, but miss i18n from Doctrine1 and
Propel? Want to make rich i18n forms or retrieve all your entity translations in one simple query?

New DoctrineExtensionsTranslator comes to rescue!

There’s already a Translatable extension in Gedmo DoctrineExtensions repo. It’s super easy to
setup and to use, when you’re talking bout single translation persist:

$article = $em->find('EntityArticle', 1 /*article id*/);
$article->setTitle('my title in de');
$article->setContent('my content in de');
$article->setTranslatableLocale('de_de'); // change locale
$em->persist($article);
$em->flush();

but becomes incredibly hard, when you’re talking bout translation outputting:

$article = $em->find('EntityArticle', 1 /*article id*/);
$repository = $em->getRepository('GedmoTranslatableEntityTranslation');
$translations = $repository->findTranslations($article);

or multiple translations writing:

$repository = $em->getRepository('Gedmo\Translatable\Entity\Translation');
$article = new Article;
$article->setTitle('My article en');
$article->setContent('content en');

$repository->translate($article, 'title', 'de', 'my article de')
    ->translate($article, 'content', 'de', 'content de')
    ->translate($article, 'title', 'ru', 'my article ru')
    ->translate($article, 'content', 'ru', 'content ru');

$em->persist($article);
$em->flush();

And if you start to think about something like multilingual forms – your work could simply become
a hell.

So, today we’re presenting another way to do i18n inside your Doctrine2-enabled project
Translator. It’s little bit more explicit to setup inside your entities, but much more easier to
use and extend later.

Let’s start with simple Person entity:

<?php

use DoctrineORMMapping as ORM;

/**
 * @ORMEntity
 */
class Person
{
    /**
     * @ORMId
     * @ORMGeneratedValue
     * @ORMColumn(type="integer")
     */
    private $id;
    /**
     * @ORMColumn(name="name", type="string", length=128)
     */
    private $name;
    /**
     * @ORMColumn(name="desc", type="string", length=128)
     */
    private $description;

    public function getId()
    {
        return $this->id;
    }

    public function setName($name)
    {
        $this->name = $name;
    }

    public function getName()
    {
        return $this->name;
    }

    public function setDescription($description)
    {
        $this->description = $description;
    }

    public function getDescription()
    {
        return $this->description;
    }
}

Lets say we want to make our name property translatable. First, we need a some place to store our
translation values. This place is a special PersonTranslation entity, which you should create by
hands:

<?php

use DoctrineORMMapping as ORM;
use GedmoTranslatorEntityTranslation;

/**
 * @ORMTable(
 *         indexes={@ORMindex(name="person_translations_lookup_idx", columns={
 *             "locale", "translatable_id"
 *         })},
 *         uniqueConstraints={@ORMUniqueConstraint(name="person_lookup_unique_idx", columns={
 *             "locale", "translatable_id", "property"
 *         })}
 * )
 * @ORMEntity
 */
class PersonTranslation extends Translation
{
    /**
     * @ORMManyToOne(targetEntity="Person", inversedBy="translations")
     */
    protected $translatable;
}

and map it to the Person entity with one-to-many relation:

/**
 * @ORMOneToMany(
 *     targetEntity="PersonTranslation",
 *     mappedBy="translatable",
 *     cascade={"persist"}
 * )
 */
private $translations;

public function __construct()
{
    $this->translations = new DoctrineCommonCollectionsArrayCollection();
}

Now you have an entity itself and the translations collection in it, which will store and persist
all your translations automatically.

Now, how to actually add/use translations of your entity? With additional translate() method
inside your entity:

public function translate($locale = null)
{
    if (null === $locale) {
        return $this;
    }

    return new GedmoTranslatorTranslationProxy($this,
    /* Locale                            */ $locale,
    /* List of translatable properties:  */ array('name'),
    /* Translation entity class:         */ 'PersonTranslation',
    /* Translations collection property: */ $this->translations
    );
}

Now you could simply write:

$person = new Person();

$person->translate()->setName('Konstantin');
$person->translate('ru')->setName('Константин');

// persist and flush entity with made translations:
$em->persist($person);
$em->flush();

Now, to retrieve and use your entity translations, just do:

$person = $em->getRepository('Person')->findOneByName('Konstantin');

// 'Konstantin' == $person->getName() == $person->translate()->getName()
// 'Константин' != $person->getName()
// 'Константин' == $person->translate('ru')->getName()

The first time you’ll require non-default translation – Doctrine2 will automatically make an
additional request to database to reach it. But what if you want to get entity with transaltions in
one query? Use joins:

$persons = $em->getRepository('Person')
    ->createQueryBuilder('p')
    ->select('p, t')
    ->join('p.translations', 't')
    ->getQuery()
    ->execute();

// no additional query:
$ruName = $persons[0]->translate('ru')->getName();

It’s that simple! But what if i want to create a i18n-enabled form or define validators on the
translatable fields? Use custom TranslationProxy:

The Symfony forms are not able to deal with non-existent [gs]etters as the ones
simulated by the use of “magic” method in the generic translation proxy so you
must create your own proxy class extending it:

class CustomProxy extends TranslationProxy
{
    public function setName($name)
    {
        return $this->setTranslatedValue('name', $name);
    }

    public function getName()
    {
        return $this->getTranslatedValue('name');
    }
}

And update the ->translate() method of the Person entity and add specific getters for the wanted translations:

public function translate($locale = null)
{
    if (null === $locale) {
        return $this;
    }

    return new CustomProxy($this,
                           $locale,
                           array('name'),
                           'PersonTranslation',
                           $this->translations
    );
}

public function getEnTranslation()
{
    return $this->translate('en');
}

public function getFrTranslation()
{
    return $this->translate('fr');
}

Now, you can create the form type associated to the Person entity:

class PersonType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('enTranslation.name', 'text')
            ->add('frTranslation.name', 'text')
        ;
    }

    public function getDefaultOptions(array $options)
    {
        return array(
            'data_class' => 'Person'
        );
    }
}

For the validation, you need to specify each translation must be valid on the Person entity:

/**
 * @AssertValid
 */
public function getEnTranslation()
{
    return $this->translate('en');
}

/**
 * @AssertValid
 */
public function getFrTranslation()
{
    return $this->translate('fr');
}

And map the constraints you want to your custom proxy’s getters:

/**
 * @AssertNotBlank
 * @AssertMaxLength(255)
 */
public function getName()
{
    return $this->getTranslatedValue('name');
}

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>