Extending Drupal Translations for Custom Entity in Drupal 7

Drupal 7 core, together with Drupal contrib (https://www.drupal.org/docs/7/modules), has the ability to ship a powerful platform for our digital requirements in the enterprise world. The multilingual feature is one of the prime reasons that Drupal is a distinguished and a preferred solution to achieve business goals. Drupal 8 includes the multilingual package in the core itself, while Drupal 7 requires some extra amount of effort to build this package as a whole to make Drupal multilingual ready. As you might know, Drupal 7 has two major ways which can help you achieve multilingual functionality: 1. Content Translation 2. Entity Translation

Content Translation vs Entity Translation

To be brief, Content Translation creates a new node when a node is translated and is the historic way to translate content. On the other hand, Entity Translation works together with the Field Translation module shipped under the Internationalization module to translate fields of a node and is the recommended way to internationalize comment. Also, don’t forget, if you’re using entity translation, you need to use the Title module to add support for translatable titles. (Read More: Quora: Difference between Content and Entity Translation Drupal Stackexchange: Difference between Content and Entity Translation ) The combination of these contrib modules helps us bring multilingual power to our Drupal Platform. General translation of Drupal content, including node, taxonomy, menu, built-in interface, configuration (variables), is achieved easily by using these modules. But Drupal is not just limited to these entities. As developers, we have the flexibility in Drupal to create custom entities as per our requirements, and hence it's our responsibility to extend all the Drupal features to these custom built entities together with maintaining the great UX that Drupal already has. But custom entity is not translation ready by default. So let’s see how we can extend translation support for a custom entity in Drupal 7.

Step 1: Add a language column in the custom entity table

We need to add a language column to the custom entity table to save the original language of the entity. Something like this: 'language' => [ 'type' => 'varchar', 'description' => "Language", 'length' => 20, 'not null' => FALSE, 'default' => LANGUAGE_NONE, ], Add this to the update hook_schema of the custom entity. And if the module already exists, provide hook_update to do so using db_add_field.

Step 2: Update hook_entity_info - entity keys & translation

In order to achieve this we need to let our Drupal know of this new power of “Multilingual Support” using Entity API. So, find hook_entity_info implemented for the custom entity in your custom module and update ‘entity keys’ key of the entity info array to include ‘language’ as well: entity-info.png 'entity keys' => [ 'id' => 'id' , 'language' => 'language', ], Also, additionally we need to provide more related information to the entity info array at top level (similar to ‘entity keys’) as:

 'uri callback' => 'entity_class_uri', 'translation' => [ 'locale' => TRUE, 'entity_translation' => [ 'class' => 'EntityTranslationDefaultHandler', 'default settings' => [ 'default_language' => LANGUAGE_NONE, 'hide_language_selector' => FALSE, ], // Base path of the custom entity. 'base path' => 'custom_entity/%custom_entity', // Translate path to be used. 'translate path' => 'custom_entity/%'custom_entity/translate', // Access Callback to check access to translate tab. 'access callback' => 'custom_entity_translation_tab_access', // This depends on the translate path used above. 'access arguments' => [1], 'edit form' => 'custom_entity', 'view path' => 'custom_entity/%custom_entity', 'edit path' => 'custom_entity/%custom_entity/edit', ], ], 

We need to define access callback 'custom_entity_translation_tab_access' function declared as ‘access callback’, which can be something like this: function 'custom_entity_translation_tab_access'($entity) { return entity_translation_tab_access('custom_entity', $entity); } Important Note: Few keys defined above under translation might be incorrect or of no use, but this combination worked for me. For detailed information, refer to following links: https://www.drupal.org/project/entity_translation/issues/2442703 https://www.drupal.org/project/entity_translation/issues/2789133


Step 3: Enable Entity Translation

The above 2 steps create a supportive backend to extend multilingual support to our custom entity. Now, let's start testing and building on top of this. So, the next step in the direction is to enable Entity Translation for this entity as well, similar to what we have for other entities at:admin/config/regional/entity_translation We can also configure default language and language selector for the entity on the entity add/edit form. But unfortunately, at the time of development, I faced a few issues regarding this, which most probably is because of entity translation module. So, I would recommend you use the following settings for your custom entity: Default Language: Current Language Hide Language Selector: Checked entity-translation.png While discussing step 5, I’ll try to elaborate more on the issues faced and the decisions made here.

Step 4: Enable Field Translations for the custom entity fields

Now some good news: Field Translation works perfectly fine here as well. Just go to manage fields via UI and edit the field and enable field translation for all the fields you want to translate. Nothing fancy over here. dn5rm.png

Step 5: Update entity add form

This is a very important step, as we are about to extend already existing custom entity form for add/edit operation. Remember in step 2, we added translation key to entity info and declared translate path. This adds a translate tab (rather say local task menu) to the entity edit/view page. translate-tab.png The Translate tab lists all languages and now we can add/edit translations for other languages. Translation actually uses the same entity add/edit form to support the addition of translation to add content in different languages. We need to update this form to support Field Translation and also to pick the correct language as per the language of the translated version of the entity being edited. Update your custom_entity_form to add a language component // ADD language variable . $langcode = entity_language('custom_entity', $entity); if (empty($langcode)) { $langcode = LANGUAGE_NONE; } // Simply use the default language. $form['language'] = array( '#type' => 'value', '#value' => $langcode, ); // This is required by other translation modules. $form_state[‘custom_entity’] = $entity; field_attach_form('custom_entity', $entity, $form, $form_state, $langcode); Note that field_attach_form has langcode parameter, which is responsible for updating field elements based on current language for the entity and the translations. Update the submit handler of the form as well. Add language property of the entity before entity save operation as follows: $entity->language = $form_state['values']['language']; $entity->save(); Great! Now we are good to test the addition and updating operation for custom entity, checking default values for the Add translation form of the entity. And if everything is working fine (basically storage operation is fine), then we are ready to move forward.

Step 6: Use of field_language function

This step is very crucial, from Multilingual support point of view. Anytime a field value is used either for display or any other activity, we generally use LANGUAGE_NONE or ‘und’ to get the value, which is not correct. As we are using field_translation module, field values now are stored considering the language value as well. Generally, field values in any entity are stored under LANGUAGE_NONE / ‘und’ and hence we fetch the value in a similar fashion. $entity->field_sample[‘und’][0][‘value’] = ‘sample value’ But, enabling field translation stores the value separately for individual languages as: $entity->field_sample['en'][0]['value'] = 'sample value'; $entity->field_sample['de'][0]['value'] = 'sample value'; Keys ‘en’ an ‘de’ represents language code for English and German Language in the above code. So, find all the places in the custom codebase where the wrong way has been used and instead use field_language function. field-language.png In case Entity Metadata Wrapper is used, then this is not required.

Step 7: Title module does not support custom entity title translation

So, custom entity translation works fine till the last step, but the whole mood is spoiled when we come to know that Title module does not support custom entity translation. https://www.drupal.org/project/title/issues/2718771 Now what? In Drupal, we have a solution for everything, if not contrib then definitely custom :D and anyway we are already transforming our custom entity. So, let’s start extending translation support to the title of a custom entity. We are basically going to implement the same functionality of the Title module ourselves (independent of the title module).

Step 7.1: Add Title field

Title module replaces the original title with title_field, in fact, it adds a field named ‘title_field’ to the entity (say node) and uses field translation on top of that. So, similarly, we will add a field for the same purpose and enable field translation for that field. Looks good? If yes, go ahead and add title field manually using field UI with whatever machine name you want. For eg: field_custom_entity_title title-field.png

Step 7.2: Update calls to retrieve title of entity

Now we need to update all the possible calls to the original title and reroute that so that whenever a label is asked we try to send translated title as per current language. One quick possible way can be using hook_entity_load and update entity’s title property based on language asked or current language. But this will not work as if you edit any custom entity then you will never be able to get actual value of title for the edit forms and will end up creating a mess. So, we will have to go stepwise. Firstly let’s create a method in our Custom Entity Controller to get translated title /** * Get translated title based on field_custom_entity_title. */ public function getTranslatedTitle($entity, $langcode = NULL) { global $language; if (empty($langcode)) { $langcode = $language->language; } $translated_title = $entity->title; if (field_info_field('field_custom_entity_title')) { if (!empty($entity->field_custom_entity_title[$langcode][0]['value'])) { $translated_title = $entity->field_custom_entity_title[$langcode][0]['value']; } } return $translated_title; } Now we need to update entity label callback as defined in hook_entity_info to use this function to return label, something like this: return entity_get_controller('custom_entity')->getTranslatedTitle($entity); The above 2 steps makes it possible to get translated title for our custom entity using entity metadata wrapper.

Step 7.3: Make use of entity_metadata_wrapper

So now wherever we have directly used $entity->title it’s time to update that and instead use entity_metadata_wrapper or entity_get_controller & getTranslatedTitle to get appropriate title as per language asked or current language. Check: https://www.drupal.org/docs/7/api/entity-api/entity-metadata-wrappers for entity_metadata_wrapper usage Now, as far as custom code is concerned we are good and are supporting translations for our custom entity. But, it might be possible that other contrib modules are not using entity_api and instead are fetching title in a manner that the original title is sent instead of the translated one. In that case, we will have to identify this by performing kind of smoke testing and create a patch to fix this by using entity_api and entity_metadata_wrapper. I was able to find 2 such relevant issues

  1. Widget entityreference_autocomplete was not showing default values with the translated title. So, I used hook_field_widget_WIDGET_TYPE_form_alter to update that. That’s a dirty fix
  2. Contrib Module Entity Autocomplete : https://www.drmodulesrg/project/entity_autocomplete/issues/3023447

Few contrib module use the label of entity keys from hook_entity_info to get label directly from custom entity table which creates a problem and hence we can propose to use entity_api whenever available to fetch title.