# Translatable behavior extension for Doctrine 2
**Translatable** behavior offers a very handy solution for translating specific record fields
in diferent languages. Further more, it loads the translations automatically for a locale
currently used, which can be set to **Translatable Listener** on it`s initialization or later
for other cases through the **Entity** itself
Features:
- Automatic storage of translations in database
- ORM and ODM support using same listener
- Automatic translation of Entity or Document fields then loaded
- ORM query can use **hint** to translate all records without issuing additional queries
- Can be nested with other behaviors
- Annotation, Yaml and Xml mapping support for extensions
[blog_reference]: http://gediminasm.org/article/translatable-behavior-extension-for-doctrine-2 "Translatable extension for Doctrine 2 makes automatic record field translations and their loading depending on language used"
[blog_test]: http://gediminasm.org/test "Test extensions on this blog"
**2012-01-28**
- Created personal translation which maps through real foreign key
constraint. This dramatically improves the management of translations
**2012-01-04**
- Refactored translatable to be able to persist, update many translations
using repository, [issue #224](https://github.com/l3pp4rd/DoctrineExtensions/issues/224)
**2011-12-11**
- Added more useful translation query hints: Override translatable locale, inner join
translations instead left join, override translation fallback
**2011-11-08**
- Thanks to [@acasademont](https://github.com/acasademont) Translatable now does not store translations for default locale. It is always left as original record value.
So be sure you do not change your default locale per project or per data migration. This way
it is more rational and unnecessary to store it additionaly in translation table.
Update **2011-04-21**
- Implemented multiple translation persistense through repository
Update **2011-04-16**
- Made an ORM query **hint** to hook into any select type query, which will join the translations
and let you **filter, order or search** by translated fields directly. It also will translate
all selected **collections or simple components** without issuing additional queries. It also
supports translation fallbacks
- For performance reasons, translation fallbacks are disabled by default
Update **2011-04-04**
- Made single listener, one instance can be used for any object manager
and any number of them
**Note list:**
- You can [test live][blog_test] on this blog
- Public [Translatable repository](http://github.com/l3pp4rd/DoctrineExtensions "Translatable extension on Github") is available on github
- Using other extensions on the same Entity fields may result in unexpected way
- May inpact your application performace since it does an additional query for translation if loaded without query hint
- Last update date: **2012-02-15**
**Portability:**
- **Translatable** is now available as [Bundle](http://github.com/stof/StofDoctrineExtensionsBundle)
ported to **Symfony2** by **Christophe Coevoet**, together with all other extensions
This article will cover the basic installation and functionality of **Translatable** behavior
Content:
- [Including](#including-extension) the extension
- Entity [example](#entity-domain-object)
- Document [example](#document-domain-object)
- [Yaml](#yaml-mapping) mapping example
- [Xml](#xml-mapping) mapping example
- Basic usage [examples](#basic-examples)
- [Persisting](#multi-translations) multiple translations
- Using ORM query [hint](#orm-query-hint)
- Advanced usage [examples](#advanced-examples)
- Personal [translations](#personal-translations)
## Setup and autoloading
Read the [documentation](http://github.com/l3pp4rd/DoctrineExtensions/blob/master/doc/annotations.md#em-setup)
or check the [example code](http://github.com/l3pp4rd/DoctrineExtensions/tree/master/example)
on how to setup and use the extensions in most optimized way.
### Translatable annotations:
- **@Gedmo\Mapping\Annotation\Translatable** it will **translate** this field
- **@Gedmo\Mapping\Annotation\TranslationEntity(class="my\class")** it will use this class to store **translations** generated
- **@Gedmo\Mapping\Annotation\Locale or @Gedmo\Mapping\Annotation\Language** this will identify this column as **locale** or **language**
used to override the global locale
## Translatable Entity example:
**Note:** that Translatable interface is not necessary, except in cases there
you need to identify entity as being Translatable. The metadata is loaded only once then
cache is activated
``` php
id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setContent($content)
{
$this->content = $content;
}
public function getContent()
{
return $this->content;
}
public function setTranslatableLocale($locale)
{
$this->locale = $locale;
}
}
```
## Translatable Document example:
``` php
id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setContent($content)
{
$this->content = $content;
}
public function getContent()
{
return $this->content;
}
public function setTranslatableLocale($locale)
{
$this->locale = $locale;
}
}
```
## Yaml mapping example
Yaml mapped Article: **/mapping/yaml/Entity.Article.dcm.yml**
```
---
Entity\Article:
type: entity
table: articles
gedmo:
translation:
locale: localeField
# using specific personal translation class:
# entity: Translatable\Fixture\CategoryTranslation
id:
id:
type: integer
generator:
strategy: AUTO
fields:
title:
type: string
length: 64
gedmo:
- translatable
content:
type: text
gedmo:
- translatable
```
## Xml mapping example
``` xml
```
## Basic usage examples: {#basic-examples}
Currently a global locale used for translations is "en_us" which was
set in **TranslationListener** globaly. To save article with its translations:
``` php
setTitle('my title in en');
$article->setContent('my content in en');
$em->persist($article);
$em->flush();
```
This inserted an article and inserted the translations for it in "en_us" locale
only if **en_us** is not the [default locale](#advanced-examples) in case if default locale
matches current locale - it uses original record value as translation
Now lets update our article in diferent locale:
``` php
find('Entity\Article', 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();
```
This updated an article and inserted the translations for it in "de_de" locale
To see and load all translations of **Translatable** Entity:
``` php
find('Entity\Article', 1 /*article id*/);
$article->setLocale('ru_ru');
$em->refresh($article);
$article = $em->find('Entity\Article', 1 /*article id*/);
$repository = $em->getRepository('Gedmo\Translatable\Entity\Translation');
$translations = $repository->findTranslations($article);
/* $translations contains:
Array (
[de_de] => Array
(
[title] => my title in de
[content] => my content in de
)
[en_us] => Array
(
[title] => my title in en
[content] => my content in en
)
)*/
```
As far as our global locale is now "en_us" and updated article has "de_de" values.
Lets try to load it and it should be translated in English
``` php
getRepository('Entity\Article')->find(1/* id of article */);
echo $article->getTitle();
// prints: "my title in en"
echo $article->getContent();
// prints: "my content in en"
```
## Persisting multiple translations
Usually it is more convinient to persist more translations when creating
or updating a record. **Translatable** allows to do that through translation repository.
All additional translations will be tracked by listener and when the flush will be executed,
it will update or persist all additional translations.
**Note:** these translations will not be processed as ordinary fields of your object,
in case if you translate a **slug** additional translation will not know how to generate
the slug, so the value as an additional translation should be processed when creating it.
### Example of multiple translations:
``` php
getRepository('Gedmo\\Translatable\\Entity\\Translation');
// it works for ODM also
$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();
// updating same article also having one new translation
$repo
->translate($article, 'title', 'lt', 'title lt')
->translate($article, 'content', 'lt', 'content lt')
->translate($article, 'title', 'ru', 'title ru change')
->translate($article, 'content', 'ru', 'content ru change')
->translate($article, 'title', 'en', 'title en (default locale) update')
->translate($article, 'content', 'en', 'content en (default locale) update')
;
$em->flush();
```
## Using ORM query hint
By default, behind the scenes, when you load a record - translatable hooks into **postLoad**
event and issues additional query to translate all fields. Imagine that, when you load a collection,
it may issue a lot of queries just to translate those fields. Including array hydration,
it is not possible to hook any **postLoad** event since it is not an
entity being hydrated. These are the main reasons why **TranslationWalker** was created.
**TranslationWalker** uses a query **hint** to hook into any **select type query**,
and when you execute the query, no matter which hydration method you use, it automatically
joins the translations for all fields, so you could use ordering filtering or whatever you
want on **translated fields** instead of original record fields.
And in result there is only one query for all this happyness.
If you use translation [fallbacks](#advanced-examples) it will be also in the same single
query and during the hydration process it will replace the empty fields in case if they
do not have a translation in currently used locale.
Now enough talking, here is an example:
``` php
createQuery($dql);
// set the translation query hint
$query->setHint(
\Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER,
'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'
);
$articles = $query->getResult(); // object hydration
$articles = $query->getArrayResult(); // array hydration
```
And even a subselect:
``` php
createQuery($dql);
$query->setHint(
\Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER,
'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'
);
```
**NOTE:** if you use memcache or apc. You should set locale and other options like fallbacks
to query through hints. Otherwise the query will be cached with a first used locale
``` php
setHint(
\Gedmo\Translatable\TranslatableListener::HINT_TRANSLATABLE_LOCALE,
'en', // take locale from session or request etc.
);
// fallback
$query->setHint(
\Gedmo\Translatable\TranslatableListener::HINT_FALLBACK,
1, // fallback to default values in case if record is not translated
);
$articles = $query->getResult(); // object hydration
```
Theres no need for any words anymore.. right?
I recommend you to use it extensively since it is a way better performance, even in
cases where you need to load single translated entity.
**Note**: Even in **COUNT** select statements translations are joined to leave a
possibility to filter by translated field, if you do not need it, just do not set
the **hint**. Also take into account that it is not possibble to translate components
in **JOIN WITH** statement, example
```
JOIN a.comments c WITH c.message LIKE '%will_not_be_translated%'`
```
**Note**: any **find** related method calls cannot hook this hint automagically, we
will use a different approach when **persister overriding feature** will be
available in **Doctrine**
In case if **translation query walker** is used, you can additionally override:
### Overriding translation fallback
``` php
setHint(Gedmo\TranslationListener::HINT_FALLBACK, 1);
```
will fallback to default locale translations instead of empty values if used.
And will override the translation listener setting for fallback.
``` php
setHint(Gedmo\TranslationListener::HINT_FALLBACK, 0);
```
will do the opposite.
### Using inner join strategy
``` php
setHint(Gedmo\TranslationListener::HINT_INNER_JOIN, true);
```
will use **INNER** joins
for translations instead of **LEFT** joins, so that in case if you do not want untranslated
records in your result set for instance.
### Overriding translatable locale
``` php
setHint(Gedmo\TranslationListener::HINT_TRANSLATABLE_LOCALE, 'en');
```
would override the translation locale used to translate the resultset.
**Note:** all these query hints lasts only for the specific query.
## Advanced examples:
### Default locale
In some cases we need a default translation as a fallback if record does not have
a translation on globaly used locale. In that case Translation Listener takes the
current value of Entity. So if **default locale** is specified and it matches the
locale in which record is being translated - it will not create extra translation
but use original values instead. If translation fallback is set to **false** it
will fill untranslated values as blanks
To set the default locale:
``` php
setDefaultLocale('en_us');
```
To set translation fallback:
``` php
setTranslationFallback(true); // default is false
```
**Note**: Default locale should be set on the **TranslatableListener** initialization
once, since it can impact your current records if it will be changed. As it
will not store extra record in translation table by default.
If you need to store translation in default locale, set:
``` php
setPersistDefaultLocaleTranslation(true); // default is false
```
This would always store translations in all locales, also keeping original record
translated field values in default locale set.
### Translation Entity
In some cases if there are thousands of records or even more.. we would like to
have a single table for translations of this Entity in order to increase the performance
on translation loading speed. This example will show how to specify a different Entity for
your translations by extending the mapped superclass.
ArticleTranslation Entity:
``` php
## Personal translations
Translatable has **AbstractPersonalTranslation** mapped superclass, which must
be extended and mapped based on your **entity** which you want to translate.
Note: translations are not automapped because of user preference based on cascades
or other possible choices, which user can make.
Personal translations uses foreign key constraint which is fully managed by ORM and
allows to have a collection of related translations. User can use it anyway he likes, etc.:
implementing array access on entity, using left join to fill collection and so on.
Note: that [query hint](#orm-query-hint) will work on personal translations the same way.
You can always use a left join like for standard doctrine collections.
Usage example:
``` php
translations = new ArrayCollection();
}
public function getTranslations()
{
return $this->translations;
}
public function addTranslation(CategoryTranslation $t)
{
if (!$this->translations->contains($t)) {
$this->translations[] = $t;
$t->setObject($this);
}
}
public function getId()
{
return $this->id;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getTitle()
{
return $this->title;
}
public function setDescription($description)
{
$this->description = $description;
}
public function getDescription()
{
return $this->description;
}
public function __toString()
{
return $this->getTitle();
}
}
```
Now the translation entity for the Category:
``` php
setLocale($locale);
$this->setField($field);
$this->setContent($value);
}
/**
* @ORM\ManyToOne(targetEntity="Category", inversedBy="translations")
* @ORM\JoinColumn(name="object_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $object;
}
```
Some example code to persist with translations:
``` php
setTitle('Food');
$food->addTranslation(new Entity\CategoryTranslation('lt', 'title', 'Maistas'));
$fruits = new Entity\Category;
$fruits->setParent($food);
$fruits->setTitle('Fruits');
$fruits->addTranslation(new Entity\CategoryTranslation('lt', 'title', 'Vaisiai'));
$fruits->addTranslation(new Entity\CategoryTranslation('ru', 'title', 'rus trans'));
$em->persist($food);
$em->persist($fruits);
$em->flush();
```
This would create translations for english and lithuanian, and for fruits, **ru** additionally.
Easy like that, any suggestions on improvements are very welcome
### Example code to use Personal Translations with (Symfony2 Sonata) i18n Forms:
Suppose you have a Sonata Backend with a simple form like:
``` php
with('General')
->add('title', 'text')
->end()
;
}
```
Then you can turn it into an 118n Form by providing the following changes.
``` php
with('General')
->add('title', 'translatable_field', array(
'field' => 'title',
'personal_translation' => 'ExampleBundle\Entity\Translation\ProductTranslation',
'property_path' => 'translations',
))
->end()
;
}
```
To accomplish this you can add the following code in your bundle:
https://gist.github.com/2437078
/Form/TranslatedFieldType.php
/Form/EventListener/addTranslatedFieldSubscriber.php
/Resources/services.yml
Then you can change to your needs:
``` php
'field' => 'title', //you need to provide which field you wish to translate
'personal_translation' => 'ExampleBundle\Entity\Translation\ProductTranslation', //the personal translation entity
```
### Translations field type using Personal Translations with Symfony2:
You can use [A2lixTranslationFormBundle](https://github.com/a2lix/TranslationFormBundle) to facilitate your translations.