Translating custom entities

Last updated on
10 July 2025

This page has not yet been reviewed by Develop maintainer(s) and added to the menu.

Translating custom entities in Drupal 10+

This is a guide on how to add translation support to custom entities in Drupal 10+.

Introduction

This write up is based on entities that extend ContentEntityBase. To create a custom content entity that supports multiple languages, you need to include the necessary components required by the translation system: language code support and certain publication elements, such as owner and status. Fortunately, most of the code is provided through traits.

Components

  • src/Entity/MyEntityInterface.php

You should add 2 interfaces to your entity interface, to have your entity properly handled by the translation system.

...
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\user\EntityOwnerInterface;
...

/**
 * ...
 */
interface MyEntityInterface extends ContentEntityInterface, EntityOwnerInterface, EntityPublishedInterface {
  ...
}
  • src/Entity/MyEntity.php

Your entity class annotation needs to be updated to include the appropriate handlers and keys, and mark your entity as translatable. You will also have to add a set of base fields: owner/uid is provided by EntityOwnerTrait and language related traits will be automatically added by the system (thanks to interface implementation) when your entity is set as translatable on the "Content language and translation" administration page (admin/config/regional/content-language).

...
use Drupal\Core\Entity\EntityPublishedTrait;
use Drupal\user\EntityOwnerTrait;
...

/**
 * Defines the MyEntity entity.
 *
 * @ingroup my_entity
 *
 * @ContentEntityType(
 *   id = "my_entity",
 *   label = @Translation("MyEntity"),
 *   handlers = {
 *    ...
 *     "translation" = "Drupal\content_translation\ContentTranslationHandler",
       // This is the controller responsible for handling the translation interface.
 *     "list_builder" = "Drupal\custom_module\MyEntityListBuilder" // To translate lists.
 *   },
 *   entity_keys = {
 *    ...
 *     "langcode" = "langcode", // Tells Drupal what you've called the langcode field.
 *     "owner" = "uid", // Required by the translation interface.
 *   },
 *   translatable = TRUE, // This needs to be set in order for the UI to pick up as translatable.
 *   ...
 * )
 */
class MyEntity extends ContentEntityBase implements MyEntityInterface {

  use EntityOwnerTrait;
  use EntityPublishedTrait;

  ...

  /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    // Base fields (id, uuid, langcode).
    $fields = parent::baseFieldDefinitions($entity_type);
    // Owner field (uid).
    $fields += static::ownerBaseFieldDefinitions($entity_type);
    ...
    // If you have custom base fields that could be translated, set them
    // translatable.
    $fields['title'] = BaseFieldDefinition::create('string')
      ...
      ->setTranslatable(TRUE);
    ...
  }

  ...
  // You *may* override the following methods *as needed*...

  /**
   * {@inheritdoc}
   */
  public function getTranslation($langcode) {
    // If you need to dynamically load translations, you need to add them to the
    // 'translations' array:
    /** @var \Drupal\custom_module\Entity\MyEntity */
    $translation = ...; // Custom logic to load translated entity.
    // Now add the new translation instance to the main default instance:
    $this->translations[$langcode]['entity'] = $translation;
    $this->translations[$langcode]['status'] = static::TRANSLATION_EXISTING;
    // And share all current translations with the new instance:
    $translation->translations = $this->translations;
    // Then, let the system do the rest.
    return parent::getTranslation($langcode);
  }

  /**
   * {@inheritdoc}
   */
  public function hasTranslation($langcode) {
    ...
  }

  /**
   * {@inheritdoc}
   */
  public function getTranslationLanguages($include_default = TRUE) {
    ...
  }

  /**
   * {@inheritdoc}
   */
  public function isTranslatable() {
    ...
  }

  ...
}
  • src/MyEntityListBuilder.php

This class will allow to automatically translate your entities in the list according to current (user) language.

...
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityListBuilder;
use Drupal\Core\Entity\EntityTypeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

...
class MyEntityListBuilder extends EntityListBuilder {
  ...

  /**
   * The language manager.
   *
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

  /**
   * {@inheritdoc}
   */
  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
    $instance = parent::createInstance($container, $entity_type);
    $instance->languageManager = $container->get('language_manager');
    return $instance;
  }

  /**
   * {@inheritdoc}
   */
  public function load() {
    $entity_ids = $this->getEntityIds();
    $current_language = $this->languageManager->getCurrentLanguage()->getId();
    $entities = [];
    foreach ($entity_ids as $entity_id) {
      $entity = $this->storage->load($entity_id);
      if (!empty($entity)) {
        $entities[$entity_id] = $entity->hasTranslation($current_language)
          ? $entity->getTranslation($current_language)
          : $entity;
      }
    }
    return $entities;
  }
  
  ...
}
  • src/MyEntityTranslationHandler.php

You may need to override some methods according to your entity specificity. Typically, ::entityFormAlter(), and ::entityFormEntityBuild(). See \Drupal\node\NodeTranslationHandler for example.

...
use Drupal\content_translation\ContentTranslationHandler;

...
class MyEntityTranslationHandler extends ContentTranslationHandler {
...
}
  • my_entity_module.install.php

 (optional) Update existing entity schema/ definition. This is only needed if you already have custom entities installed.

/**
 * Implements hook_update_N().
 *
 * Adds Translatable fields to my_entity.
 */
function my_entity_module_update_10001(&$sandbox) {
  $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();

  $field_storage_definition = $entity_definition_update_manager->getFieldStorageDefinition(
    'title', // The field name to update.
    'my_entity' // The target entity to update.
  );
  $field_storage_definition->setTranslatable(TRUE);
  $entity_definition_update_manager->updateFieldStorageDefinition($field_storage_definition);

You must also update the entity schema. This should be done in a post update.

Within your module add a post update file. EX: my_entity_module.post_update.php

See attached file example.

Now rebuild your site, and run drush updatedb to apply these changes

Issues:

1) When loading entities check first if the entity is translatable, and whether a translation exists currently.

Example: normal query

public function loadEntityByProperties($properties) { 
 $entity = \Drupal::entityTypeManager()
      ->getStorage($bundle)
      ->loadByProperties($properties);

 $entity->getTranslation($langcode);
}

The above snippet won't work because it will check if a translation exists for the sites current langcode. If the language doesn't exist on the entity prior to loading, it throws an Exception.

\InvalidArgumentException("Invalid translation language ($langcode) specified.");

This will break your site if you have any entities that still need to be translated.

What we need to do is check the entity isTranslatable and hasTranslation before calling getTranslation, otherwise return the entity in it's original language.

I suggest creating a helper function to use when loading entities so that you don't need to write this out every time.

Solution: new query to pull translation.

public function loadEntityByProperties($properties) {
    $entity = \Drupal::entityTypeManager()
      ->getStorage($bundle)
      ->loadByProperties($properties);    

    $lang = \Drupal::languageManager()->getCurrentLanguage()->getId();

    $entity_translation = [];

    foreach ($entities as $entity) {
      if ($entity->isTranslatable() && $entity->hasTranslation($lang)) {
        $entity_translation[] = $entity->getTranslation($lang);
      }
      else {
        $entity_translation[] = $entity;
      }
    }

    return $entity_translation;
}

Help improve this page

Page status: No known problems

You can: