Commit 1886e7c1 authored by fago's avatar fago

Issue #1356978 added basic i18n integration to be used by provided entity types.

parent 65487628
......@@ -117,6 +117,11 @@
* Features module integration for exportable entities. The given class has to
* inherit from the default class being EntityDefaultFeaturesController. Set
* it to FALSE to disable this feature.
* - i18n controller class: (optional) A controller class for providing
* i18n module integration for (exportable) entities. The given class has to
* inherit from the class EntityDefaultI18nStringController. Defaults to
* FALSE (disabled). See EntityDefaultI18nStringController for more
* information.
* - views controller class: (optional) A controller class for providing views
* integration. The given class has to inherit from the class
* EntityDefaultViewsController, which is set as default in case the providing
......
<?php
/**
* @file
* Internationalization (i18n) integration.
*/
/**
* Gets the i18n controller for a given entity type.
*
* @return EntityDefaultI18nStringController|array|false
* If a type is given, the controller for the given entity type. Else an array
* of all enabled controllers keyed by entity type is returned.
*/
function entity_i18n_controller($type = NULL) {
$static = &drupal_static(__FUNCTION__);
if (!isset($type)) {
// Invoke the function for each type to ensure we have fully populated the
// static variable.
foreach (entity_get_info() as $entity_type => $info) {
entity_i18n_controller($entity_type);
}
return array_filter($static);
}
if (!isset($static[$type])) {
$info = entity_get_info($type);
// Do not activate it by default. Modules have to explicitly enable it by
// specifying EntityDefaultI18nStringController or their customization.
$class = isset($info['i18n controller class']) ? $info['i18n controller class'] : FALSE;
$static[$type] = $class ? new $class($type, $info) : FALSE;
}
return $static[$type];
}
/**
* Implements hook_i18n_string_info().
*/
function entity_i18n_string_info() {
$groups = array();
foreach (entity_i18n_controller() as $entity_type => $controller) {
$groups += $controller->hook_string_info();
}
return $groups;
}
/**
* Implements hook_i18n_object_info().
*/
function entity_i18n_object_info() {
$info = array();
foreach (entity_i18n_controller() as $entity_type => $controller) {
$info += $controller->hook_object_info();
}
return $info;
}
/**
* Implements hook_i18n_string_objects().
*/
function entity_i18n_string_objects($type) {
if ($controller = entity_i18n_controller($type)) {
return $controller->hook_string_objects();
}
}
/**
* Default controller handling i18n integration.
*
* Implements i18n string translation for all non-field properties marked as
* 'translatable' and having the flag 'i18n string' set. This translation
* approach fits in particular for translating configuration, i.e. exportable
* entities.
*
* Requirements for the default controller:
* - The entity type providing module must be specified using the 'module' key
* in hook_entity_info().
* - An 'entity class' derived from the provided class 'Entity' must be used.
* - Properties must be declared as 'translatable' and the 'i18n string' flag
* must be set to TRUE using hook_entity_property_info().
* - i18n must be notified about changes manually by calling
* i18n_string_object_update(), i18n_string_object_remove() and
* i18n_string_update_context(). Ideally, this is done in a small integration
* module depending on the entity API and i18n_string. Look at the provided
* testing module "entity_test_i18n" for an example.
* - If the entity API admin UI is used, the "translate" tab will be
* automatically enabled and linked from the UI.
* - There are helpers for getting translated values which work regardless
* whether the i18n_string module is enabled, i.e. entity_i18n_string()
* and Entity::getTranslation().
*
* Current limitations:
* - Translatable property values cannot be updated via the metadata wrapper,
* however reading works fine. See Entity::getTranslation().
*/
class EntityDefaultI18nStringController {
protected $entityType, $entityInfo;
/**
* The i18n textgroup we are using.
*/
protected $textgroup;
public function __construct($type) {
$this->entityType = $type;
$this->entityInfo = entity_get_info($type);
// By default we go with the module name as textgroup.
$this->textgroup = $this->entityInfo['module'];
}
/**
* Implements hook_i18n_string_info() via entity_i18n_string_info().
*/
public function hook_string_info() {
$list = system_list('module_enabled');
$info = $list[$this->textgroup]->info;
$groups[$this->textgroup] = array(
'title' => $info['name'],
'description' => !empty($info['description']) ? $info['description'] : NULL,
'format' => FALSE,
'list' => TRUE,
);
return $groups;
}
/**
* Implements hook_i18n_object_info() via entity_i18n_object_info().
*
* Go with the same default values as the admin UI as far as possible.
*/
public function hook_object_info() {
$wildcard = $this->menuWildcard();
$id_key = !empty($this->entityInfo['entity keys']['name']) ? $this->entityInfo['entity keys']['name'] : $this->entityInfo['entity keys']['id'];
$info[$this->entityType] = array(
// Generic object title.
'title' => $this->entityInfo['label'],
// The object key field.
'key' => $id_key,
// Placeholders for automatic paths.
'placeholders' => array(
$wildcard => $id_key,
),
// Properties for string translation.
'string translation' => array(
// Text group that will handle this object's strings.
'textgroup' => $this->textgroup,
// Object type property for string translation.
'type' => $this->entityType,
// Translatable properties of these objects.
'properties' => $this->translatableProperties(),
),
);
// Integrate the translate tab into the admin-UI if enabled.
if ($base_path = $this->menuBasePath()) {
$info[$this->entityType] += array(
// To produce edit links automatically.
'edit path' => $base_path . '/manage/' . $wildcard,
// Auto-generate translate tab.
'translate tab' => $base_path . '/manage/' . $wildcard . '/translate',
);
$info[$this->entityType]['string translation'] += array(
// Path to translate strings to every language.
'translate path' => $base_path . '/manage/' . $wildcard . '/translate/%i18n_language',
);
}
return $info;
}
/**
* Defines the menu base path used by self::hook_object_info().
*/
protected function menuBasePath() {
return !empty($this->entityInfo['admin ui']['path']) ? $this->entityInfo['admin ui']['path'] : FALSE;
}
/**
* Defines the menu wildcard used by self::hook_object_info().
*/
protected function menuWildcard() {
return isset($this->entityInfo['admin ui']['menu wildcard']) ? $this->entityInfo['admin ui']['menu wildcard'] : '%entity_object';
}
/**
* Defines translatable properties used by self::hook_object_info().
*/
protected function translatableProperties() {
$list = array();
foreach (entity_get_all_property_info($this->entityType) as $name => $info) {
if (!empty($info['translatable']) && !empty($info['i18n string'])) {
$list[$name] = array(
'title' => $info['label'],
);
}
}
return $list;
}
/**
* Implements hook_i18n_string_objects() via entity_i18n_string_objects().
*/
public function hook_string_objects() {
return entity_load_multiple_by_name($this->entityType, FALSE);
}
}
......@@ -2,6 +2,7 @@ name = Entity API
description = Enables modules to work with any entity type and to provide entities.
core = 7.x
files[] = entity.features.inc
files[] = entity.i18n.inc
files[] = entity.info.inc
files[] = entity.rules.inc
files[] = entity.test
......
......@@ -1070,6 +1070,23 @@ function entity_ui_get_form($entity_type, $entity, $op = 'edit', $form_state = a
return drupal_build_form($form_id, $form_state);
}
/**
* Helper for using i18n_string().
*
* @param $name
* Textgroup and context glued with ':'.
* @param $default
* String in default language. Default language may or may not be English.
* @param $langcode
* (optional) The code of a certain language to translate the string into.
* Defaults to the i18n_string() default, i.e. the current language.
*
* @see i18n_string()
*/
function entity_i18n_string($name, $default, $langcode = NULL) {
return function_exists('i18n_string') ? i18n_string($name, $default, array('langcode' => $langcode)) : $default;
}
/**
* Implements hook_views_api().
*/
......
......@@ -428,6 +428,107 @@ class EntityAPIRulesIntegrationTestCase extends EntityWebTestCase {
}
}
/**
* Test the i18n integration.
*/
class EntityAPIi18nItegrationTestCase extends EntityWebTestCase {
public static function getInfo() {
return array(
'name' => 'Entity CRUD i18n integration',
'description' => 'Tests the i18n integration provided by the Entity CRUD API.',
'group' => 'Entity API',
'dependencies' => array('i18n_string'),
);
}
function setUp() {
parent::setUp('entity_test_i18n');
$this->admin_user = $this->drupalCreateUser(array('bypass node access', 'administer nodes', 'administer languages', 'administer content types', 'administer blocks', 'access administration pages'));
$this->drupalLogin($this->admin_user);
$this->addLanguage('de');
}
/**
* Copied from i18n module (class Drupali18nTestCase).
*
* We cannot extend from Drupali18nTestCase as else the test-bot would die.
*/
public function addLanguage($language_code) {
// Check to make sure that language has not already been installed.
$this->drupalGet('admin/config/regional/language');
if (strpos($this->drupalGetContent(), 'enabled[' . $language_code . ']') === FALSE) {
// Doesn't have language installed so add it.
$edit = array();
$edit['langcode'] = $language_code;
$this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
// Make sure we are not using a stale list.
drupal_static_reset('language_list');
$languages = language_list('language');
$this->assertTrue(array_key_exists($language_code, $languages), t('Language was installed successfully.'));
if (array_key_exists($language_code, $languages)) {
$this->assertRaw(t('The language %language has been created and can now be used. More information is available on the <a href="@locale-help">help screen</a>.', array('%language' => $languages[$language_code]->name, '@locale-help' => url('admin/help/locale'))), t('Language has been created.'));
}
}
elseif ($this->xpath('//input[@type="checkbox" and @name=:name and @checked="checked"]', array(':name' => 'enabled[' . $language_code . ']'))) {
// It's installed and enabled. No need to do anything.
$this->assertTrue(true, 'Language [' . $language_code . '] already installed and enabled.');
}
else {
// It's installed but not enabled. Enable it.
$this->assertTrue(true, 'Language [' . $language_code . '] already installed.');
$this->drupalPost(NULL, array('enabled[' . $language_code . ']' => TRUE), t('Save configuration'));
$this->assertRaw(t('Configuration saved.'), t('Language successfully enabled.'));
}
}
/**
* Tests the provided default controller.
*/
function testDefaultController() {
// Create test entities for the user1 and unrelated to a user.
$entity = entity_create('entity_test_type', array(
'name' => 'test',
'uid' => $GLOBALS['user']->uid,
'label' => 'label-en',
));
$entity->save();
// Add a translation.
i18n_string_textgroup('entity_test')->update_translation("entity_test_type:{$entity->name}:label", 'de', 'label-de');
$default = entity_i18n_string("entity_test:entity_test_type:{$entity->name}:label", 'label-en');
$translation = entity_i18n_string("entity_test:entity_test_type:{$entity->name}:label", 'label-en', 'de');
$this->assertEqual($translation, 'label-de', 'Label has been translated.');
$this->assertEqual($default, 'label-en', 'Default label retrieved.');
// Test the helper method.
$translation = $entity->getTranslation('label', 'de');
$default = $entity->getTranslation('label');
$this->assertEqual($translation, 'label-de', 'Label has been translated via the helper method.');
$this->assertEqual($default, 'label-en', 'Default label retrieved via the helper method.');
// Test updating and make sure the translation stays.
$entity->name = 'test2';
$entity->save();
$translation = $entity->getTranslation('label', 'de');
$this->assertEqual($translation, 'label-de', 'Translation survives a name change.');
// Test using the wrapper to retrieve a translation.
$wrapper = entity_metadata_wrapper('entity_test_type', $entity);
$translation = $wrapper->language('de')->label->value();
$this->assertEqual($translation, 'label-de', 'Translation retrieved via the wrapper.');
// Test deleting.
$entity->delete();
$translation = entity_i18n_string("entity_test:entity_test_type:{$entity->name}:label", 'label-en', 'de');
$this->assertEqual($translation, 'label-en', 'Translation has been deleted.');
}
}
/**
* Tests metadata wrappers.
......
......@@ -224,6 +224,46 @@ class Entity {
return entity_get_controller($this->entityType)->buildContent($this, $view_mode, $langcode);
}
/**
* Gets the raw, translated value of a property or field.
*
* Supports retrieving field translations as well as i18n string translations.
*
* Note that this returns raw data values, which might not reflect what
* has been declared for hook_entity_property_info() as no 'getter callbacks'
* are invoked or no referenced entities are loaded. For retrieving values
* reflecting the property info make use of entity metadata wrappers, see
* entity_metadata_wrapper().
*
* @param $property_name
* The name of the property to return; e.g., 'title'.
* @param $langcode
* (optional) The language code of the language to which the value should
* be translated. If set to NULL, the default display language is being
* used.
*
* @return
* The raw, translated property value; or the raw, un-translated value if no
* translation is available.
*
* @todo Implement an analogous setTranslation() method for updating.
*/
public function getTranslation($property, $langcode = NULL) {
$all_info = entity_get_all_property_info($this->entityType);
$property_info = $all_info[$property];
if (!empty($property_info['translatable'])) {
if (!empty($property_info['field'])) {
return field_get_items($this->entityType, $this, $property, $langcode);
}
elseif (!empty($property_info['i18n string'])) {
$name = $this->entityInfo['module'] . ':' . $this->entityType . ':' . $this->identifier() . ':' . $property;
return entity_i18n_string($name, $this->$property, $langcode);
}
}
return $this->$property;
}
/**
* Magic method to only serialize what's necessary.
*/
......
......@@ -370,7 +370,14 @@ function entity_property_verbatim_get($data, array $options, $name, $type, $info
return $data[$name];
}
elseif (is_object($data) && isset($data->$name)) {
return $data->$name;
// Incorporate i18n_string translations. We may rely on the entity class
// here as its usage is required by the i18n integration.
if (isset($options['language']) && !empty($info['i18n string'])) {
return $data->getTranslation($name, $options['language']->language);
}
else {
return $data->$name;
}
}
return NULL;
}
......
......@@ -220,28 +220,41 @@ class EntityDefaultUIController {
foreach ($entities as $entity) {
$rows[] = $this->overviewTableRow($conditions, entity_id($this->entityType, $entity), $entity);
}
// Assemble the right table header.
$header = array(t('Label'));
if (!empty($this->entityInfo['exportable'])) {
$header[] = t('Status');
}
// Add operations with the right colspan.
$field_ui = !empty($this->entityInfo['bundle of']) && module_exists('field_ui');
$exportable = !empty($this->entityInfo['exportable']);
$colspan = 3;
$colspan = $field_ui ? $colspan + 2 : $colspan;
$colspan = $exportable ? $colspan + 1 : $colspan;
$header[] = array('data' => t('Operations'), 'colspan' => $colspan);
$render = array(
'#theme' => 'table',
'#header' => $header,
'#header' => $this->overviewTableHeaders($conditions, $rows),
'#rows' => $rows,
'#empty' => t('None.'),
);
return $render;
}
/**
* Generates the table headers for the overview table.
*/
protected function overviewTableHeaders($conditions, $rows, $additional_header = array()) {
$header = $additional_header;
array_unshift($header, t('Label'));
if (!empty($this->entityInfo['exportable'])) {
$header[] = t('Status');
}
// Add operations with the right colspan.
$header[] = array('data' => t('Operations'), 'colspan' => $this->operationCount());
return $header;
}
/**
* Returns the operation count for calculating colspans.
*/
protected function operationCount() {
$count = 3;
$count += !empty($this->entityInfo['bundle of']) && module_exists('field_ui') ? 2 : 0;
$count += !empty($this->entityInfo['exportable']) ? 1 : 0;
$count += !empty($this->entityInfo['i18n controller class']) ? 1 : 0;
return $count;
}
/**
* Generates the row for the passed entity and may be overridden in order to
* customize the rows.
......@@ -276,13 +289,12 @@ class EntityDefaultUIController {
$field_ui = !empty($this->entityInfo['bundle of']) && module_exists('field_ui');
// For exportable entities we add an export link.
$exportable = !empty($this->entityInfo['exportable']);
$colspan = 3;
$colspan = $field_ui ? $colspan + 2 : $colspan;
$colspan = $exportable ? $colspan + 1 : $colspan;
// If i18n integration is enabled, add a link to the translate tab.
$i18n = !empty($this->entityInfo['i18n controller class']);
// Add operations depending on the status.
if (entity_has_status($this->entityType, $entity, ENTITY_FIXED)) {
$row[] = array('data' => l(t('clone'), $this->path . '/manage/' . $id . '/clone'), 'colspan' => $colspan);
$row[] = array('data' => l(t('clone'), $this->path . '/manage/' . $id . '/clone'), 'colspan' => $this->operationCount());
}
else {
$row[] = l(t('edit'), $this->path . '/manage/' . $id);
......@@ -291,10 +303,13 @@ class EntityDefaultUIController {
$row[] = l(t('manage fields'), $this->path . '/manage/' . $id . '/fields');
$row[] = l(t('manage display'), $this->path . '/manage/' . $id . '/display');
}
if ($i18n) {
$row[] = l(t('translate'), $this->path . '/manage/' . $id . '/translate');
}
if ($exportable) {
$row[] = l(t('clone'), $this->path . '/manage/' . $id . '/clone');
}
if (empty($this->entityInfo['exportable']) || !entity_has_status($this->entityType, $entity, ENTITY_IN_CODE)) {
$row[] = l(t('delete'), $this->path . '/manage/' . $id . '/delete', array('query' => drupal_get_destination()));
}
......
......@@ -29,6 +29,7 @@ function entity_test_entity_info() {
'bundle keys' => array(
'bundle' => 'name',
),
'module' => 'entity_test',
),
'entity_test_type' => array(
'label' => t('Test entity type'),
......@@ -42,6 +43,7 @@ function entity_test_entity_info() {
'id' => 'id',
'name' => 'name',
),
'module' => 'entity_test',
),
);
......
name = Entity-test type translation
description = Allows translating entity-test types.
dependencies[] = entity_test
dependencies[] = i18n_string
package = Multilingual - Internationalization
core = 7.x
hidden = TRUE
\ No newline at end of file
<?php
/**
* @file
* Entity-test i18n integration module via entity API i18n support.
*
* @see EntityDefaultI18nController
*/
/**
* Implements hook_entity_info_alter().
*/
function entity_test_i18n_entity_info_alter(&$info) {
// Enable i18n support via the entity API.
$info['entity_test_type']['i18n controller class'] = 'EntityDefaultI18nStringController';
}
/**
* Implements hook_entity_property_info_alter().
*/
function entity_test_i18n_entity_property_info_alter(&$info) {
// Mark some properties as translatable, but also denote that translation
// works with i18n_string.
foreach (array('label') as $name) {
$info['entity_test_type']['properties'][$name]['translatable'] = TRUE;
$info['entity_test_type']['properties'][$name]['i18n string'] = TRUE;
}
}
/**
* Implements hook_{entity_test_type}_insert().
*/
function entity_test_i18n_entity_test_type_insert($test_type) {
i18n_string_object_update('entity_test_type', $test_type);
}
/**
* Implements hook_{entity_test_type}_update().
*/
function entity_test_i18n_entity_test_type_update($test_type) {
// Account for name changes.
if ($test_type->original->name != $test_type->name) {
i18n_string_update_context("entity_test:entity_test_type:{$test_type->original->name}:*", "entity_test:entity_test_type:{$test_type->name}:*");
}
i18n_string_object_update('entity_test_type', $test_type);
}
/**
* Implements hook_{entity_test_type}_delete().
*/
function entity_test_i18n_entity_test_type_delete($test_type) {
i18n_string_object_remove('entity_test_type', $test_type);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment