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:

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:

Query 1: (0.6 ms)
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

Translation Strategies: CakePHP's TranslateBehavior supports two main approaches:
  • 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
This example demonstrates the shadow table strategy for better query performance and clarity.

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

Send your feedback or bugreport!