Greetings! I'm trying to solve a problem i have for 2 days already, but without luck. I checked every single thread on it could, but still didn't find an answer. Please help!

The question is: how to translate custom entity using Entity translation module?
P.S. i checked commerce product for reference, but really don't get it.

I have a custom entity module, and i want to make that custom entity multilingual (i want some fields to be translatable on my custom entity for different languages). So i thought i can use Entity Translation module to do just that. I create simple module to test things out. Here is complete code:

test.info:

name = Test module
description = Test module to test entity trasnlation
core = 7.x

test.install

<?php

/**
 * Implements hook_schema().
 */
function test_schema() {

  $schema = array();

  $schema['test_custom_entity'] = array(
    'description' => 'The base table for the Custom entity',
    'fields' => array(
      'id' => array(
        'description' => 'Primary key of the Custom entity',
        'type' => 'serial',
        'unsigned' => TRUE,
        'not null' => TRUE,
      ),
      'name' => array(
        'description' => 'Custom entity name.',
        'type' => 'varchar',
        'length' => 255,
        'not null' => FALSE,
      ),     
    ),
    'primary key' => array('id'),
  );

  return $schema;
}
?>

test.module

<?php

/**
 * Implements hook_entity_info().
 */
function test_entity_info()
{
    $info = array();

    $info['custom_entity'] = array(
        'label' => t('Custom entity'),
        'base table' => 'test_custom_entity',        
        'entity keys' => array(
            'id' => 'id',
            'label' => 'name', 
        ),
        'module' => 'test', 
        'entity class' => 'Entity', 
        'controller class' => 'EntityAPIController', 
        'admin ui' => array(
            'path' => 'admin/custom_entities',
            'controller class' => 'EntityDefaultUIController',
        ),        
        'access callback' => 'test_access_callback',                
        'fieldable' => TRUE,
        'bundles' => array(
          'custom_entity' => array(
            'label' => t('Custom entity'),
            'admin' => array(
              'path' => 'admin/custom_entities',
            ),
          ),
        ),
        'translation' => array(            
            'entity_translation' => array(
                'class' => 'EntityTranslationDefaultHandler',
                'default settings' => array(
                    'default_language' => LANGUAGE_NONE,
                    'hide_language_selector' => FALSE,
                ),
                'base path' => 'admin/custom_entities/manage/%',
                'path wildcard' => '%',
                'access callback' => 'test_entity_translation_tab_access'
            ),
        ),
    );

    return $info;
}

function custom_entity_form($form, &$form_state, $entity = NULL)
{
    $form['name'] = array(
        '#title' => t('Entry name'),
        '#type' => 'textfield',
        '#default_value' => isset($entity->name) ? $entity->name : '',
        '#required' => TRUE,
    );

    field_attach_form('custom_entity', $entity, $form, $form_state);
    
    $form['actions'] = [
        '#type' => 'actions',
        'submit' => [
            '#type' => 'submit',
            '#value' => isset($crpc->id) ? t('Update') : t('Save'),
        ]
    ];     

    return $form;
}

/**
 * Submit handler for the add/edit form.
 */
function custom_entity_form_submit($form, &$form_state)
{
    $entity = entity_ui_form_submit_build_entity($form, $form_state);
    $entity->save();
    drupal_set_message(t('The entry: @name has been saved.', array('@name' => $entity->name)));
    $form_state['redirect'] = 'admin/custom_entities';
}

function test_permission()
{
    return [
        'administer test' => [
            t('Administer Test module')
        ]        
    ];
}

/**
 * Check access permissions for cr product compatibles
 */
function test_access_callback($op, $entity = null, $account = null)
{
    if (user_access('administer test', $account)) {
        return true;
    }
    
    return false;
}

/**
 * Access callback: determines access to a product's translation tab.
 */
function test_entity_translation_tab_access($entity) {
  return entity_translation_tab_access('custom_entity', $entity);
}
?>

I suspect that I have problem with 'base path'. I don't understand it's conception. I thought it will work like this:

'base path' => 'admin/custom_entities/%custom_entity'

but it doesn't. Also i don't understand what means %custom_entity, because i don't see it in the URL. Is it related to url at all?

Here is description of the problem i have:

Currently i was managed to get `Translate` tab only when i use following configuration. I came to this kinda randomly, but it works:

'base path' => 'admin/custom_entities/manage/%',
'path wildcard' => '%',

At the same time I don't know why it doesn't work like that: 'base path' => 'admin/custom_entities/%custom_entity'

But even if Translate tab is available, it still doesn't work:

Here is custom-entity-entry edit form:
custom entity edit form

Here is custom-entity-translate-tab:
custom entity translate tab

Here is what happens when i go to `edit` link or `add` link, empty values, no content!:
custom entity translate tab

I'm trying to understand what am i doing wrong in my custom entity so it doesn't show entity values in my entity-translation form (as far as i see when debugging it uses integer value instead of entity object, but i have no clue why, probably base path problem?) and how to understand what base path is? Thanks!

Comments

nickolas.wilson’s picture

Looking over your code, I'd give a good guess it is due to missing the following things. It took me many sleepless nights to get this to work myself. The Documentation on this area really does suck.

Anyway, this is what has worked for me...

yourmodule.install >

/**
 * Implements hook_schema().
 */
function test_schema() {
  
$schema = array();
  
$schema['test_custom_entity'] = array(
    'description' => 'The base table for the Custom entity',
    'fields' => array(
      'id' => array(
        'description' => 'Primary key of the Custom entity',
        'type' => 'serial',
        'unsigned' => TRUE,
        'not null' => TRUE,
      ),

// ADD this field.
      'language' => array(
        'type' => 'varchar',
        'length' => 32,
        'not null' => FALSE,
      ),      

      'name' => array(
        'description' => 'Custom entity name.',
        'type' => 'varchar',
        'length' => 255,
        'not null' => FALSE,
      ),     
    ),
    'primary key' => array('id'),
  );
  return 

$schema;
}

yourmodule.module >

hook_entity_info() >





function test_entity_info() {
    $info = array();
    
    $info['custom_entity'] = array(
        'label' => t('Custom entity'),
        'base table' => 'test_custom_entity',        
       
        //ADD language to entity key.

        'entity keys' => array(
            'id' => 'id',
            'label' => 'name',
            'language' => 'language',
        ),
        'module' => 'test', 
        'entity class' => 'Entity', 
        'controller class' => 'EntityAPIController', 
        'admin ui' => array(
            'path' => 'admin/custom_entities',
            'controller class' => 'EntityDefaultUIController',
        ),        
        'access callback' => 'test_access_callback',                
        'fieldable' => TRUE,
        'bundles' => array(
          'custom_entity' => array(
            'label' => t('Custom entity'),
            'admin' => array(
              'path' => 'admin/custom_entities',
            ),
          ),
        ),
        'translation' => array(            
            'entity_translation' => array(
                'class' => 'EntityTranslationDefaultHandler',
                'default settings' => array(
                    'default_language' => LANGUAGE_NONE,
                    'hide_language_selector' => FALSE,
                ),

                //ADD %custom_entity

                'base path' => 'admin/custom_entities/manage/%custom_entity',
                'path wildcard' => '%custom_entity',  //Edited
                'access callback' => 'test_entity_translation_tab_access'
            ),
        ),
    );
  return $info;
}

hook_entity_form() >>


/**
* Implements hook_form().
*/

function custom_entity_form($form, &$form_state, $entity = NULL) {
    
   // ADD language variable .

   $langcode = entity_language('custom_entity', $entity);

   if (empty($langcode)) {
     $langcode = LANGUAGE_NONE;
   }
 
  // ADD language to form. 
  $form['language'] = array(
    '#type' => 'value',
    '#value' => $langcode,
  ); 

    $form['name'] = array(
        '#title' => t('Entry name'),
        '#type' => 'textfield',
        '#default_value' => isset($entity->name) ? $entity->name : '',
        '#required' => TRUE,
    );

  
   // ADD Language  $langcode to end of field attach.

   field_attach_form('custom_entity', $entity, $form, $form_state, $langcode);
    
    $form['actions'] = [
        '#type' => 'actions',
        'submit' => [
            '#type' => 'submit',
            '#value' => isset($crpc->id) ? t('Update') : t('Save'),
        ]
    ];     
    return $form;
}

Hopefully that will fix your problems.

Also, make sure you have activated the custom entity on the Entity Translation config page (your.domain.com/admin/config/regional/entity_translation)... I've forgotten a few times and it drove me nuts... :)

EDIT:

After writing this, I thought I'd better go through my module. It turns out that, I have a problem with adding new translations.. So there may be problems with my answer above. Though, I do think it will fix some of the problems you are having. Soon as I have worked out my problems, I'll update it here.

phpcoder2015’s picture

Thank you Nickolas! Yeah, i tested it out and there are still problems. As I said probably something is wrong with 'base path' I use. In Commerce Product i see that during debugging Entity object is passed to edit form, while in my code it passes entity id, this is why i can't see entity-entry values. But i don't know how to fix it yet, unfortunately.

I also think that

'base path' => 'admin/custom_entities/manage/%custom_entity',
'path wildcard' => '%',

should match, like

'base path' => 'admin/custom_entities/manage/%',
'path wildcard' => '%',

This is why i specify 'path wildcard' explicitly, to let system know that i use custom path wildcard. Probably this is also a point where problem happens. Still don't know why normal '%custom_entity' doesn't work. Actually translate tab only appears in each entity-entry (where it's needed) when i use that latest piece of code above:

'base path' => 'admin/custom_entities/manage/%',
'path wildcard' => '%',

but as i said, in that case only entity id instead of entity object is passed to translate-edit-trasnlation form which turns into empty entity-entry values. So this makes me think i need to adapt '%custom_entity' because it somehow says system to pass entity object instead of entity-id param like '3'. Probably another question is which mechanism causes param-to-entity-object conversion.

nickolas.wilson’s picture

Oh, I made a mistake. (I'll change it above as well as noting it here.)

'base path' => 'admin/custom_entities/manage/%custom_entity',
'path wildcard' => '%',

Should of been this >

'base path' => 'admin/custom_entities/manage/%custom_entity',
'path wildcard' => '%custom_entity',

From my understanding of the function of 'path wildcard' is to set your alias of your entity type so it can retrieve the entity id/identifier. This is only required when you have multiple bundles and need different alias for each one. I think your understanding of the function is incorrect.

From API info:
* - path wildcard: The menu router path wildcard identifying the entity.
* Defaults to %ENTITY_TYPE.

From what you just wrote, you are saying that your form does not retrieve the data from the DB?

Have you tested your entity with translate disabled?

phpcoder2015’s picture

CRUD works fine on custom-entity-entries itself, but if i use:

	'base path' => 'admin/custom_entities/manage/%custom_entity',
	'path wildcard' => '%custom_entity',

or as you mentioned, we can simply specify 'base path' without 'path wildcard' since entity id is as expected - '%custom_entity':

	'base path' => 'admin/custom_entities/manage/%custom_entity'	

However, in that case 'TRANSLATE' tab appears inside 'Custom entitys' list, instead of inside inside each custom-entity-entry:

custom entity TRANSLATEtab wrong location

This initially was a problem and i started to try different combinations with base path like 'admin/custom_entities/manage/%' after which 'TRANSLATE' tab appears where it's needed, but still doesn't work correctly.

nickolas.wilson’s picture

Ummm, how about adding 'translate path'...

  'translation' => array(            
            'entity_translation' => array(
                'class' => 'EntityTranslationDefaultHandler',
                'default settings' => array(
                    'default_language' => LANGUAGE_NONE,
                    'hide_language_selector' => FALSE,
                ),
                'base path' => 'admin/custom_entities/manage/%custom_entity',
                'translate path' => 'admin/custom_entities/manage/%custom_entity/translate',
                'path wildcard' => '%custom_entity',
                'access callback' => 'test_entity_translation_tab_access'
            ),
        ),

This is a guess, but it might sort out the problem of the translate tab's location.

phpcoder2015’s picture

No, it unfortunately still displays 'TRANSLATE' tab on `custom-entitys` list page (as far as i know all other paths are based on 'base path' so we can omit it in configuration array). Maybe explicit hook_menu() entry is needed to make everything work, but i'm not sure. I'll try to re-investigate Commerce Product module and it's Commerce Product UI companion module.

Btw i'm surprised that custom entities translation is such an uncommon topic. I thought a lot of people need it or is there another way to store multilingual content in custom db table? I tried nodes for this but performance is so low that it takes a lot to load nodes i need on single page request, so i wanted to create custom entity instead (since i need custom db table and admin UI), but obviously it is troublesome at the moment since there are no good basic examples..

phpcoder2015’s picture

Didn't fully test it yet because I'm heavily into Drupal 8 now, but this is what I did and now tab content appears correctly for both add and edit forms. Just needed to append these 2 functions inside test.module:

/**
 * Loads a custom-entity by ID.
 */
function custom_entity_load($entity_id) {
  if (empty($entity_id)) {
    return FALSE;
  }

  $entities = custom_entity_load_multiple(array($entity_id), array());
  return $entities ? reset($entities) : FALSE;
}

/**
 * Loads multiple custom-entities by ID or based on a set of matching conditions.
 *
 * @see entity_load()
 *
 * @param $entity_ids
 *   An array of entity IDs.
 * @param $conditions
 *   An array of conditions on the {custom_entity} table in the form
 *     'field' => $value.
 * @param $reset
 *   Whether to reset the internal custom-entity loading cache.
 *
 * @return
 *   An array of custom-entity objects indexed by entity_id.
 */
function custom_entity_load_multiple($entity_ids = array(), $conditions = array(), $reset = FALSE) {
  if (empty($entity_ids) && empty($conditions)) {
    return array();
  }

  return entity_load('custom_entity', $entity_ids, $conditions, $reset);
}

I took it from commerce product module and adapted to my custom-entity.

How did I came up to this? This is excerpt from Pro Drupal 7 Development book:

$items['user/%user_uid_only_optional'] = array(
  'title' => 'My account',
  'title callback' => 'user_page_title',
  'title arguments' => array(1),
  'page callback' => 'user_view_page',
  'page arguments' => array(1),
  'access callback' => 'user_view_access',
  'access arguments' => array(1),
  'weight' => -10,
  'menu_name' => 'user-menu',
);
When Drupal creates the menu using user/%user_uid_only_optional, it replaces the
%user_uid_only_optional using the process as described below:
  1. In the second segment, match the string after the % and before the next possible slash. In this case, the string would be user_uid_optional.
  2. Append _load to the string to generate the name of a function. In this case, the
    name of the function is user_uid_optional_load.
  3. Call the function and pass it, as a parameter, the value of the wildcard in the
    Drupal path. So if the URL is http://example.com/?q=user/2375, the Drupal
    path is user/2375, and the wildcard matches the second segment, which is
    2375. So a call is made to user_uid_optional_load('2375').
  4. The result of this call is then used in place of the wildcard. So when the title
    callback is called with the title arguments of array(1), instead of passing part 1
    of the Drupal path (2375), we pass the result of the call to
    user_uid_optional_load('2375'), which is a user object. Think of it as a
    portion of the Drupal path being replaced by the object it represents.
  5. Note that the page and access callbacks will also use the replacement object.
    So in the previous menu item, user_view_access() will be called for access and
    user_view() will be called to generate the page content, and both will be
    passed the user object for user 2375.

So this is why i had (for example) '1' (integer value) passed as $entity_object instead of actual object. So to fix this you need to use _load function as explained in the steps above.

Hope it will help to someone, since i spent several days trying to figure everything out :)

phpcoder2015’s picture

Status: Active » Closed (fixed)
falco010’s picture

This helped me out a lot guys, thank you very much!