Translate Behavior
The Translate Behavior allows you to create and retrieve translated versions of your entities in multiple languages. Perfect for building multilingual applications.
Live Translation Demo
Select a language to see the same article content in different translations:
Language Selector
Translated Content - English
Welcome to CakePHP
CakePHP is a rapid development framework for PHP that provides an extensible architecture for developing, maintaining, and deploying applications.
Current Locale:
en
SQL Queries Executed
These queries show how TranslateBehavior retrieves translated content using the shadow table:
SELECT
`DemoArticles`.`id` AS `DemoArticles__id`,
`DemoArticles`.`title` AS `DemoArticles__title`,
`DemoArticles`.`content` AS `DemoArticles__content`,
`DemoArticles`.`status` AS `DemoArticles__status`,
`DemoArticles`.`created` AS `DemoArticles__created`,
`DemoArticles`.`modified` AS `DemoArticles__modified`
FROM
`demo_articles` `DemoArticles`
WHERE
`DemoArticles`.`id` = 1
LIMIT
1
Setup Instructions
- Shadow Table (shown here): Parallel table with same structure as main table plus locale
- EAV/I18n Table (default): Shared table using Entity-Attribute-Value pattern
1. Create Database Tables
Create your main table and a shadow translation table with matching structure:
// Migration: demo_articles table
$table = $this->table('demo_articles');
$table->addColumn('title', 'string', ['limit' => 255, 'null' => false]);
$table->addColumn('content', 'text', ['null' => false]);
$table->addColumn('status', 'string', ['limit' => 255, 'null' => false]);
$table->addColumn('created', 'datetime', ['null' => false]);
$table->addColumn('modified', 'datetime', ['null' => false]);
$table->create();
// Migration: demo_articles_translations (shadow table with matching fields + locale)
$translationsTable = $this->table('demo_articles_translations', [
'id' => false,
'primary_key' => ['id', 'locale'],
]);
$translationsTable->addColumn('id', 'integer', ['null' => false]);
$translationsTable->addColumn('locale', 'string', ['limit' => 6, 'null' => false]);
$translationsTable->addColumn('title', 'string', ['limit' => 255, 'null' => true]);
$translationsTable->addColumn('content', 'text', ['null' => true]);
$translationsTable->create();
2. Add Translate Behavior to Your Table
In your Table class initialize() method, configure the shadow table:
public function initialize(array $config): void {
parent::initialize($config);
// TranslateBehavior will automatically detect and use the shadow table
// if a table named {table_name}_translations exists with the right structure
$this->addBehavior('Translate', [
'fields' => ['title', 'content'], // Fields to translate
]);
}
3. Save Translations
When creating or updating an entity, provide translations using _translations:
$article = $articlesTable->newEntity([
'title' => 'Welcome', // Default language (en)
'content' => 'This is the content in English.',
'status' => 'published',
'_translations' => [
'de' => [
'title' => 'Willkommen',
'content' => 'Dies ist der Inhalt auf Deutsch.',
],
'es' => [
'title' => 'Bienvenido',
'content' => 'Este es el contenido en español.',
],
],
]);
$articlesTable->save($article);
4. Retrieve Translated Content
Use the translations finder with the desired locale:
// Set application locale
I18n::setLocale('de');
// Find article - automatically returns German translation
$article = $articlesTable->get($id);
echo $article->title; // Output: "Willkommen"
// Or use the finder explicitly
$article = $articlesTable->find('translations', [
'locales' => 'de' // Locale as string
])->where(['id' => $id])->first();
5. Update Existing Translations
Patch the entity with new translation data:
$article = $articlesTable->get($id);
$article = $articlesTable->patchEntity($article, [
'_translations' => [
'fr' => [
'title' => 'Bienvenue',
'content' => 'Ceci est le contenu en français.',
],
],
]);
$articlesTable->save($article);
Advanced Features
Instead of a dedicated table, you can use the shared i18n table (default):
// First, run migration: bin/cake migrations migrate -p CakeDC/I18n
$this->addBehavior('Translate', [
'fields' => ['title', 'content'],
// 'translationTable' => 'I18n', // This is the default if omitted
]); The shared i18n table is simpler to set up but dedicated tables offer better organization.
Choose between subquery (default), join, or select strategy:
$this->addBehavior('Translate', [
'fields' => ['title', 'content'],
'strategy' => 'join', // More efficient for single locale
]); Control whether to allow empty translation fields:
$this->addBehavior('Translate', [
'fields' => ['title', 'content'],
'allowEmptyTranslations' => false, // Don't save empty values
]); Tips
- Default locale translations are stored in the main table
- Non-default locales are stored in the shadow table (same structure + locale)
- Shadow tables provide better query performance than EAV/i18n tables
- Use
I18n::setLocale()to set the application-wide locale - The behavior automatically filters results based on the current locale
- Translations are loaded lazily - only when needed
- CakePHP auto-detects shadow tables if named
{table}_translations

