The Translate behavior allows you to create and retrieve translated copies of your entities in multiple languages.
Warning
The TranslateBehavior does not support composite primary keys at this point in time.
The behavior offers two strategies for how the translations are stored.
Eav Strategy: This strategy uses a i18n
table where it stores the
translation for each of the fields of any given Table object that it’s bound to.
This is currently the default strategy used by the behavior.
Shadow table Strategy: This strategy use a separate “shadow table” for each Table object to store translation of all translated fields of that table.
In order to use the Eav strategy, you need to create a i18n
table with the
correct schema. Currently the only way of loading the i18n
table is by
manually running the following SQL script in your database:
CREATE TABLE i18n (
id int NOT NULL auto_increment,
locale varchar(6) NOT NULL,
model varchar(255) NOT NULL,
foreign_key int(10) NOT NULL,
field varchar(255) NOT NULL,
content text,
PRIMARY KEY (id),
UNIQUE INDEX I18N_LOCALE_FIELD(locale, model, foreign_key, field),
INDEX I18N_FIELD(model, foreign_key, field)
);
The schema is also available as sql file in /config/schema/i18n.sql.
A note on language abbreviations: The Translate Behavior doesn’t impose any
restrictions on the language identifier, the possible values are only restricted
by the locale
column type/size. locale
is defined as varchar(6)
in
case you want to use abbreviations like es-419
(Spanish for Latin America,
language abbreviation with area code UN M.49).
Tip
It’s wise to use the same language abbreviations as required for
Internationalization and Localization. Thus you are
consistent and switching the language works identical for both, the
Translate Behaviour
and Internationalization and Localization
.
So it’s recommended to use either the two letter ISO code of the language like
en
, fr
, de
or the full locale name such as fr_FR
, es_AR
,
da_DK
which contains both the language and the country where it is spoken.
Let’s assume we have an articles
table and we want it’s title
and body
fields to be translated. For that we create a shadow table articles_translations
:
CREATE TABLE `articles_translations` (
`id` int(11) NOT NULL,
`locale` varchar(5) NOT NULL,
`title` varchar(255) NOT NULL,
`body` text NOT NULL,
PRIMARY KEY (`id`,`locale`)
);
So basically the shadow table needs id
and locale
columns which together
form the primary key and other columns with same name as primary table which
need to be translated.
Attaching the behavior can be done in the initialize()
method in your Table
class:
class ArticlesTable extends Table
{
public function initialize(array $config): void
{
// By default Eav strategy will be used.
$this->addBehavior('Translate', ['fields' => ['title', 'body']]);
}
}
The first thing to note is that you are required to pass the fields
key in
the configuration array. This list of fields is needed to tell the behavior what
columns will be able to store translations.
If you want to use the shadow table strategy then you can configure the behavior as:
class ArticlesTable extends Table
{
public function initialize(array $config): void
{
$this->addBehavior('Translate', [
'strategyClass' => \Cake\ORM\Behavior\Translate\ShadowTableStrategy::class,
]);
}
}
For shadow table strategy specifying the fields
key is optional as the
behavior can infer the fields from the shadow table columns.
By default the locale specified in App.defaultLocale
config is used as default
locale for the TranslateBehavior
. You can override that by setting defaultLocale
config of the behavior:
class ArticlesTable extends Table
{
public function initialize(array $config): void
{
$this->addBehavior('Translate', [
'defaultLocale' => 'en_GB',
]);
}
}
Note
For historical reasons and to maintain backwards compatibility, by default
the TranslateBehavior
uses the EavStrategy
. But for new projects it’s
recommended to use the ShadowTableStrategy
as it’s more efficient. You can
use TranslateBehavior::setDefaultStrategyClass(ShadowTableStrategy::class)
in your Application::bootstrap()
to change the default strategy and avoid
having to specify the strategyClass
config each time.
Regardless of the datastructure strategy you choose the behavior provides the same API to manage translations.
Now, select a language to be used for retrieving entities by changing the application language, which will affect all translations:
// In the Articles controller. Change the locale to Spanish, for example
I18n::setLocale('es');
Then, get an existing entity:
$article = $this->Articles->get(12);
echo $article->title; // Echoes 'A title', not translated yet
Next, translate your entity:
$article->title = 'Un Artículo';
$this->Articles->save($article);
You can try now getting your entity again:
$article = $this->Articles->get(12);
echo $article->title; // Echoes 'Un Artículo', yay piece of cake!
Working with multiple translations can be done by using a special trait in your Entity class:
use Cake\ORM\Behavior\Translate\TranslateTrait;
use Cake\ORM\Entity;
class Article extends Entity
{
use TranslateTrait;
}
Now you can find all translations for a single entity:
$article = $this->Articles->find('translations')->first();
echo $article->translation('es')->title; // 'Un Artículo'
echo $article->translation('en')->title; // 'An Article';
And save multiple translations at once:
$article->translation('es')->title = 'Otro Título';
$article->translation('fr')->title = 'Un autre Titre';
$this->Articles->save($article);
If you want to go deeper on how it works or how to tune the behavior for your needs, keep on reading the rest of this chapter.
If you wish to use a table other than i18n
for translating a particular
repository, you can specify the name of the table class name for your custom
table in the behavior’s configuration. This is common when you have multiple
tables to translate and you want a cleaner separation of the data that is stored
for each different table:
class ArticlesTable extends Table
{
public function initialize(array $config): void
{
$this->addBehavior('Translate', [
'fields' => ['title', 'body'],
'translationTable' => 'ArticlesI18n'
]);
}
}
You need to make sure that any custom table you use has the columns field
,
foreign_key
, locale
and model
.
As shown above you can use the setLocale()
method to choose the active
translation for entities that are loaded:
// Load I18n core functions at the beginning of your Articles Controller:
use Cake\I18n\I18n;
// Then you can change the language in your action:
I18n::setLocale('es');
// All entities in results will contain spanish translation
$results = $this->Articles->find()->all();
This method works with any finder in your tables. For example, you can
use TranslateBehavior with find('list')
:
I18n::setLocale('es');
$data = $this->Articles->find('list')->toArray();
// Data will contain
[1 => 'Mi primer artículo', 2 => 'El segundo artículo', 15 => 'Otro articulo' ...]
// Change the locale to french for a single find call
$data = $this->Articles->find('list', ['locale' => 'fr'])->toArray();
New in version 4.1.0: The locale
option was added in 4.1.0
When building interfaces for updating translated content, it is often helpful to
show one or more translation(s) at the same time. You can use the
translations
finder for this:
// Find the first article with all corresponding translations
$article = $this->Articles->find('translations')->first();
In the example above you will get a list of entities back that have a
_translations
property set. This property will contain a list of translation
data entities. For example the following properties would be accessible:
// Outputs 'en'
echo $article->_translations['en']->locale;
// Outputs 'title'
echo $article->_translations['en']->field;
// Outputs 'My awesome post!'
echo $article->_translations['en']->body;
A more elegant way for dealing with this data is by adding a trait to the entity class that is used for your table:
use Cake\ORM\Behavior\Translate\TranslateTrait;
use Cake\ORM\Entity;
class Article extends Entity
{
use TranslateTrait;
}
This trait contains a single method called translation
, which lets you
access or create new translation entities on the fly:
// Outputs 'title'
echo $article->translation('en')->title;
// Adds a new translation data entity to the article
$article->translation('de')->title = 'Wunderbar';
You can limit the languages that are fetched from the database for a particular set of records:
$results = $this->Articles->find('translations', [
'locales' => ['en', 'es']
]);
$article = $results->first();
$spanishTranslation = $article->translation('es');
$englishTranslation = $article->translation('en');
Translation records can contain any string, if a record has been translated and stored as an empty string (‘’) the translate behavior will take and use this to overwrite the original field value.
If this is undesired, you can ignore translations which are empty using the
allowEmptyTranslations
config key:
class ArticlesTable extends Table
{
public function initialize(array $config): void
{
$this->addBehavior('Translate', [
'fields' => ['title', 'body'],
'allowEmptyTranslations' => false
]);
}
}
The above would only load translated data that had content.
It is also possible to find translations for any association in a single find operation:
$article = $this->Articles->find('translations')->contain([
'Categories' => function ($query) {
return $query->find('translations');
}
])->first();
// Outputs 'Programación'
echo $article->categories[0]->translation('es')->name;
This assumes that Categories
has the TranslateBehavior attached to it. It
simply uses the query builder function for the contain
clause to use the
translations
custom finder in the association.
calling I18n::setLocale('es');
changes the default locale for all translated
finds, there may be times you wish to retrieve translated content without
modifying the application’s state. For these scenarios use the behavior’s
setLocale()
method:
I18n::setLocale('en'); // reset for illustration
// specific locale.
$this->Articles->setLocale('es');
$article = $this->Articles->get(12);
echo $article->title; // Echoes 'Un Artículo', yay piece of cake!
Note that this only changes the locale of the Articles table, it would not affect the language of associated data. To affect associated data it’s necessary to call the method on each table, for example:
I18n::setLocale('en'); // reset for illustration
$this->Articles->setLocale('es');
$this->Articles->Categories->setLocale('es');
$data = $this->Articles->find('all', ['contain' => ['Categories']]);
This example also assumes that Categories
has the TranslateBehavior attached
to it.
TranslateBehavior does not substitute find conditions by default. You need to use
translationField()
method to compose find conditions on translated fields:
$this->Articles->setLocale('es');
$query = $this->Articles->find()->where([
$this->Articles->translationField('title') => 'Otro Título'
]);
The philosophy behind the TranslateBehavior is that you have an entity representing the default language, and multiple translations that can override certain fields in such entity. Keeping this in mind, you can intuitively save translations for any given entity. For example, given the following setup:
// in src/Model/Table/ArticlesTable.php
class ArticlesTable extends Table
{
public function initialize(array $config): void
{
$this->addBehavior('Translate', ['fields' => ['title', 'body']]);
}
}
// in src/Model/Entity/Article.php
class Article extends Entity
{
use TranslateTrait;
}
// In the Articles Controller
$article = new Article([
'title' => 'My First Article',
'body' => 'This is the content',
'footnote' => 'Some afterwords'
]);
$this->Articles->save($article);
So, after you save your first article, you can now save a translation for it, there are a couple ways to do it. The first one is setting the language directly into the entity:
$article->_locale = 'es';
$article->title = 'Mi primer Artículo';
$this->Articles->save($article);
After the entity has been saved, the translated field will be persisted as well, one thing to note is that values from the default language that were not overridden will be preserved:
// Outputs 'This is the content'
echo $article->body;
// Outputs 'Mi primer Artículo'
echo $article->title;
Once you override the value, the translation for that field will be saved and can be retrieved as usual:
$article->body = 'El contendio';
$this->Articles->save($article);
The second way to use for saving entities in another language is to set the default language directly to the table:
$article->title = 'Mi Primer Artículo';
$this->Articles->setLocale('es');
$this->Articles->save($article);
Setting the language directly in the table is useful when you need to both retrieve and save entities for the same language or when you need to save multiple entities at once.
It is a common requirement to be able to add or edit multiple translations to
any database record at the same time. This can be done using the
TranslateTrait
:
use Cake\ORM\Behavior\Translate\TranslateTrait;
use Cake\ORM\Entity;
class Article extends Entity
{
use TranslateTrait;
}
Now, You can populate translations before saving them:
$translations = [
'fr' => ['title' => "Un article"],
'es' => ['title' => 'Un artículo']
];
foreach ($translations as $lang => $data) {
$article->translation($lang)->set($data, ['guard' => false]);
}
$this->Articles->save($article);
And create form controls for your translated fields:
// In a view template.
<?= $this->Form->create($article); ?>
<fieldset>
<legend>French</legend>
<?= $this->Form->control('_translations.fr.title'); ?>
<?= $this->Form->control('_translations.fr.body'); ?>
</fieldset>
<fieldset>
<legend>Spanish</legend>
<?= $this->Form->control('_translations.es.title'); ?>
<?= $this->Form->control('_translations.es.body'); ?>
</fieldset>
In your controller, you can marshal the data as normal:
$article = $this->Articles->newEntity($this->request->getData());
$this->Articles->save($article);
This will result in your article, the french and spanish translations all being
persisted. You’ll need to remember to add _translations
into the
$_accessible
fields of your entity as well.
When attaching TranslateBehavior
to a model, you can define the validator
that should be used when translation records are created/modified by the
behavior during newEntity()
or patchEntity()
:
class ArticlesTable extends Table
{
public function initialize(array $config): void
{
$this->addBehavior('Translate', [
'fields' => ['title'],
'validator' => 'translated'
]);
}
}
The above will use the validator created by validationTranslated
to
validated translated entities.