summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorfago2012-02-14 17:37:29 (GMT)
committerfago2012-02-14 17:37:29 (GMT)
commit1886e7c1e23a324fedc91b960b402e6176eccdda (patch)
tree35cb6bb558108f5c29d974d6dfdbac2386d20772
parent654876282fecaf669e4e2152f632ee62697377c0 (diff)
Issue #1356978 added basic i18n integration to be used by provided entity types.
-rw-r--r--entity.api.php5
-rw-r--r--entity.i18n.inc210
-rw-r--r--entity.info1
-rw-r--r--entity.module17
-rw-r--r--entity.test101
-rw-r--r--includes/entity.inc40
-rw-r--r--includes/entity.property.inc9
-rw-r--r--includes/entity.ui.inc51
-rw-r--r--tests/entity_test.module2
-rw-r--r--tests/entity_test_i18n.info7
-rw-r--r--tests/entity_test_i18n.module53
11 files changed, 477 insertions, 19 deletions
diff --git a/entity.api.php b/entity.api.php
index fb239cb..46471cf 100644
--- a/entity.api.php
+++ b/entity.api.php
@@ -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
diff --git a/entity.i18n.inc b/entity.i18n.inc
new file mode 100644
index 0000000..8347eaa
--- /dev/null
+++ b/entity.i18n.inc
@@ -0,0 +1,210 @@
+<?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);
+ }
+}
diff --git a/entity.info b/entity.info
index f658098..0cae8cb 100644
--- a/entity.info
+++ b/entity.info
@@ -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
diff --git a/entity.module b/entity.module
index 6cdc817..0531a1e 100644
--- a/entity.module
+++ b/entity.module
@@ -1071,6 +1071,23 @@ function entity_ui_get_form($entity_type, $entity, $op = 'edit', $form_state = a
}
/**
+ * 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().
*/
function entity_views_api() {
diff --git a/entity.test b/entity.test
index 81df878..3d667c1 100644
--- a/entity.test
+++ b/entity.test
@@ -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.
diff --git a/includes/entity.inc b/includes/entity.inc
index 0c2c480..7cbac12 100644
--- a/includes/entity.inc
+++ b/includes/entity.inc
@@ -225,6 +225,46 @@ class Entity {
}
/**
+ * 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.
*/
public function __sleep() {
diff --git a/includes/entity.property.inc b/includes/entity.property.inc
index 0268808..1d08f04 100644
--- a/includes/entity.property.inc
+++ b/includes/entity.property.inc
@@ -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;
}
diff --git a/includes/entity.ui.inc b/includes/entity.ui.inc
index b16317e..30d3c86 100644
--- a/includes/entity.ui.inc
+++ b/includes/entity.ui.inc
@@ -220,22 +220,10 @@ 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.'),
);
@@ -243,6 +231,31 @@ class EntityDefaultUIController {
}
/**
+ * 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()));
}
diff --git a/tests/entity_test.module b/tests/entity_test.module
index 2b28640..4076a97 100644
--- a/tests/entity_test.module
+++ b/tests/entity_test.module
@@ -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',
),
);
diff --git a/tests/entity_test_i18n.info b/tests/entity_test_i18n.info
new file mode 100644
index 0000000..b1f6500
--- /dev/null
+++ b/tests/entity_test_i18n.info
@@ -0,0 +1,7 @@
+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
diff --git a/tests/entity_test_i18n.module b/tests/entity_test_i18n.module
new file mode 100644
index 0000000..2aa3736
--- /dev/null
+++ b/tests/entity_test_i18n.module
@@ -0,0 +1,53 @@
+<?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);
+}