updated core to 7.58 (right after the site was hacked)

This commit is contained in:
2018-04-20 23:48:40 +02:00
parent 18f4aba146
commit 9344a61b61
711 changed files with 99690 additions and 480 deletions

View File

@@ -0,0 +1,8 @@
.tmgmt-entity-sources-wrapper .form-item {
float: left;
margin: 0 10px 0 0;
}
.tmgmt-entity-sources-wrapper #edit-search-submit {
margin: 26px 10px 0 10px;
}

View File

@@ -0,0 +1,12 @@
name = "Entity source plugin tests"
description = "Support module for entity source testing."
package = Testing
core = 7.x
hidden = TRUE
; Information added by Drupal.org packaging script on 2016-09-21
version = "7.x-1.0-rc2+1-dev"
core = "7.x"
project = "tmgmt"
datestamp = "1474446494"

View File

@@ -0,0 +1,20 @@
<?php
/**
* @file
* Tests module for the Entity Source plugin.
*/
/**
* Implements hook_entity_info_alter().
*/
function tmgmt_entity_test_entity_info_alter(&$entity_info) {
if (isset($entity_info['taxonomy_term']) && empty($entity_info['taxonomy_term']['translation'])) {
$entity_info['taxonomy_term']['translation'] = array(
'tmgmt_entity_test_translation' => array(
'base path' => 'taxonomy/term/%taxonomy_term',
'alias' => TRUE,
),
);
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* @file
* Hooks provided by the Entity Translation Management module.
*/
/**
* @addtogroup tmgmt_source
* @{
*/
/**
* Allows to alter $query used to list entities on specific entity type overview
* pages.
*
* @see TMGMTEntityDefaultSourceUIController
*/
function hook_tmgmt_entity_type_list_query_alter(EntityFieldQuery $query) {
$query->entityCondition('type', array('article', 'page'));
}
/**
* @} End of "addtogroup tmgmt_source".
*/

View File

@@ -0,0 +1,27 @@
name = Entity Source
description = Entity source plugin for the Translation Management system.
package = Translation Management
core = 7.x
dependencies[] = tmgmt
dependencies[] = tmgmt_field
dependencies[] = entity
dependencies[] = entity_translation
test_dependencies[] = pathauto
test_dependencies[] = file_entity
test_dependencies[] = entityreference
files[] = tmgmt_entity.source.test
files[] = tmgmt_entity.source.none.test
files[] = tmgmt_entity.pathauto.test
files[] = tmgmt_entity.suggestions.test
files[] = tmgmt_entity.plugin.inc
files[] = tmgmt_entity.ui.inc
; Information added by Drupal.org packaging script on 2016-09-21
version = "7.x-1.0-rc2+1-dev"
core = "7.x"
project = "tmgmt"
datestamp = "1474446494"

View File

@@ -0,0 +1,332 @@
<?php
/**
* @file
* Source plugin for the Translation Management system that handles entities.
*/
/**
* Implements hook_tmgmt_source_plugin_info().
*/
function tmgmt_entity_tmgmt_source_plugin_info() {
$info['entity'] = array(
'label' => t('Entity'),
'description' => t('Source handler for entities.'),
'plugin controller class' => 'TMGMTEntitySourcePluginController',
'item types' => array(),
);
$entity_types = array_filter(variable_get('entity_translation_entity_types', array()));
foreach ($entity_types as $entity_key) {
$entity_info = entity_get_info($entity_key);
$info['entity']['item types'][$entity_key] = $entity_info['label'];
}
return $info;
}
/**
* Implements hook_form_ID_alter().
*
* Alters comment node type select box to filter out comment types that belongs
* to non entity translatable node types.
*/
function tmgmt_entity_form_tmgmt_ui_entity_source_comment_overview_form_alter(&$form, &$form_state) {
if (!isset($form['search_wrapper']['search']['node_type'])) {
return;
}
// Change the select name to "type" as in the query submitted value will be
// passed into node.type condition.
$form['search_wrapper']['search']['type'] = $form['search_wrapper']['search']['node_type'];
unset($form['search_wrapper']['search']['node_type']);
// Set new default value.
$form['search_wrapper']['search']['type']['#default_value'] = isset($_GET['type']) ? $_GET['type'] : NULL;
}
/**
* Helper function to get entity translatable bundles.
*
* Note that for comment entity type it will return the same as for node as
* comment bundles have no use (i.e. in queries).
*
* @param string $entity_type
* Drupal entity type.
*
* @return array
* Array of key => values, where key is type and value its label.
*/
function tmgmt_entity_get_translatable_bundles($entity_type) {
// If given entity type does not have entity translations enabled, no reason
// to continue.
if (!in_array($entity_type, variable_get('entity_translation_entity_types', array()))) {
return array();
}
$entity_info = entity_get_info($entity_type);
$translatable_bundle_types = array();
foreach ($entity_info['bundles'] as $bundle_type => $bundle_definition) {
if ($entity_type == 'comment') {
$bundle_type = str_replace('comment_node_', '', $bundle_type);
if (variable_get('language_content_type_' . $bundle_type) == ENTITY_TRANSLATION_ENABLED) {
$translatable_bundle_types[$bundle_type] = $bundle_definition['label'];
}
}
elseif ($entity_type == 'node') {
if (variable_get('language_content_type_' . $bundle_type) == ENTITY_TRANSLATION_ENABLED) {
$translatable_bundle_types[$bundle_type] = $bundle_definition['label'];
}
}
else {
$translatable_bundle_types[$bundle_type] = $bundle_definition['label'];
}
}
return $translatable_bundle_types;
}
/**
* Gets translatable entities of a given type.
*
* Additionally you can specify entity property conditions, pager and limit.
*
* @param string $entity_type
* Drupal entity type.
* @param array $property_conditions
* Entity properties. There is no value processing so caller must make sure
* the provided entity property exists for given entity type and its value
* is processed.
* @param bool $pager
* Flag to determine if pager will be used.
*
* @return array
* Array of translatable entities.
*/
function tmgmt_entity_get_translatable_entities($entity_type, $property_conditions = array(), $pager = FALSE) {
if (!in_array($entity_type, variable_get('entity_translation_entity_types', array()))) {
return array();
}
$languages = drupal_map_assoc(array_keys(language_list()));
$entity_info = entity_get_info($entity_type);
$label_key = isset($entity_info['entity keys']['label']) ? $entity_info['entity keys']['label'] : NULL;
$id_key = $entity_info['entity keys']['id'];
$query = db_select($entity_info['base table'], 'e');
$query->addTag('tmgmt_entity_get_translatable_entities');
$query->addField('e', $id_key);
// Language neutral entities are not translatable. Filter them out. To do
// that: join {entity_translation} table, but only records with source column
// empty. The {entity_translation}.language will represent the original entity
// language in that case.
$source_table_alias = $query->leftJoin('entity_translation', NULL, "%alias.entity_type = :entity_type AND %alias.entity_id = e.$id_key AND %alias.source = ''", array(':entity_type' => $entity_type));
$query->condition("$source_table_alias.language", LANGUAGE_NONE, '<>');
// Searching for sources with missing translation.
if (!empty($property_conditions['target_status']) && !empty($property_conditions['target_language']) && in_array($property_conditions['target_language'], $languages)) {
$translation_table_alias = db_escape_field('et_' . $property_conditions['target_language']);
$query->leftJoin('entity_translation', $translation_table_alias, "%alias.entity_type = :entity_type AND %alias.entity_id = e.$id_key AND %alias.language = :language",
array(':entity_type' => $entity_type, ':language' => $property_conditions['target_language']));
// Exclude entities with having source language same as the target language
// we search for.
$query->condition('e.language', $property_conditions['target_language'], '<>');
if ($property_conditions['target_status'] == 'untranslated_or_outdated') {
$or = db_or();
$or->isNull("$translation_table_alias.language");
$or->condition("$translation_table_alias.translate", 1);
$query->condition($or);
}
elseif ($property_conditions['target_status'] == 'outdated') {
$query->condition("$translation_table_alias.translate", 1);
}
elseif ($property_conditions['target_status'] == 'untranslated') {
$query->isNull("$translation_table_alias.language");
}
}
// Remove the condition so we do not try to add it again below.
unset($property_conditions['target_language']);
unset($property_conditions['target_status']);
// Searching for the source label.
if (!empty($label_key) && isset($property_conditions[$label_key])) {
$search_tokens = explode(' ', $property_conditions[$label_key]);
$or = db_or();
foreach ($search_tokens as $search_token) {
$search_token = trim($search_token);
if (strlen($search_token) > 2) {
$or->condition($label_key, "%$search_token%", 'LIKE');
}
}
if ($or->count() > 0) {
$query->condition($or);
}
unset($property_conditions[$label_key]);
}
// Searching by taxonomy bundles - we need to switch to vid as the bundle key.
if ($entity_type == 'taxonomy_term' && !empty($property_conditions['vocabulary_machine_name'])) {
$property_name = 'vid';
$vocabulary = taxonomy_vocabulary_machine_name_load($property_conditions['vocabulary_machine_name']);
$property_value = $vocabulary->vid;
$query->condition('e.' . $property_name, $property_value);
// Remove the condition so we do not try to add it again below.
unset($property_conditions['vocabulary_machine_name']);
}
// Searching by the node bundles - that applies for node entities as well as
// comment.
elseif (in_array($entity_type, array('comment', 'node'))) {
$node_table_alias = 'e';
// For comments join node table so that we can filter based on type.
if ($entity_type == 'comment') {
$query->join('node', 'n', 'e.nid = n.nid');
$node_table_alias = 'n';
}
// Get translatable node types and check if it is worth to continue.
$translatable_node_types = array_keys(tmgmt_entity_get_translatable_bundles('node'));
if (empty($translatable_node_types)) {
return array();
}
// If we have type property add condition.
if (isset($property_conditions['type'])) {
$query->condition($node_table_alias . '.type', $property_conditions['type']);
// Remove the condition so we do not try to add it again below.
unset($property_conditions['type']);
}
// If not, query db only for translatable node types.
else {
$query->condition($node_table_alias . '.type', $translatable_node_types);
}
}
// Add remaining query conditions which are expected to be handled in a
// generic way.
foreach ($property_conditions as $property_name => $property_value) {
$query->condition('e.' . $property_name, $property_value);
}
if ($pager) {
$query = $query->extend('PagerDefault')->limit(variable_get('tmgmt_source_list_limit', 20));
}
else {
$query->range(0, variable_get('tmgmt_source_list_limit', 20));
}
$query->orderBy($entity_info['entity keys']['id'], 'DESC');
$entity_ids = $query->execute()->fetchCol();
$entities = array();
if (!empty($entity_ids)) {
$entities = entity_load($entity_type, $entity_ids);
}
return $entities;
}
/**
* Implements hook_tmgmt_source_suggestions()
*/
function tmgmt_entity_tmgmt_source_suggestions(array $items, TMGMTJob $job) {
$suggestions = array();
// Get all translatable entity types.
$entity_types = array_filter(variable_get('entity_translation_entity_types', array()));
foreach ($items as $item) {
if (($item instanceof TMGMTJobItem) && ($item->plugin == 'entity') || ($item->plugin == 'node')) {
// Load the entity and extract the bundle name to get all fields from the
// current entity.
$entity = entity_load_single($item->item_type, $item->item_id);
list(, , $bundle) = entity_extract_ids($item->item_type, $entity);
$field_instances = field_info_instances($item->item_type, $bundle);
// Loop over all fields, check if they are NOT translatable. Only if a
// field is not translatable we may suggest a referenced entity. If so,
// check for a supported field type (image and file currently here).
foreach ($field_instances as $instance) {
$field = field_info_field($instance['field_name']);
$field_type = $field['type'];
$field_name = $field['field_name'];
switch ($field_type) {
case 'file':
case 'image':
// 'File' (and images) must be translatable entity types.
// Other files we not suggest here. Get all field items from the
// current field and suggest them as translatable.
if (isset($entity_types['file']) && ($field_items = field_get_items($item->item_type, $entity, $field_name))) {
// Add all files as a suggestion.
foreach ($field_items as $field_item) {
$file_entity = entity_load_single('file', $field_item['fid']);
// Check if there is already a translation available for this
// file. If so, just continue with the next file.
$handler = entity_translation_get_handler('file', $file_entity);
if ($handler instanceof EntityTranslationHandlerInterface) {
$translations = $handler->getTranslations();
if (isset($translations->data[$job->target_language])) {
continue;
}
}
// Add the translation as a suggestion.
$suggestions[] = array(
'job_item' => tmgmt_job_item_create('entity', 'file', $file_entity->fid),
'reason' => t('Field @label', array('@label' => $instance['label'])),
'from_item' => $item->tjiid,
);
}
}
break;
case 'entityreference':
$target_type = $field['settings']['target_type'];
// Make sure only tranlatable entity types are suggested.
if (isset($entity_types[$target_type]) && ($field_items = field_get_items($item->item_type, $entity, $field_name))) {
// Add all referenced entities as suggestion.
foreach ($field_items as $field_item) {
$ref_entity = entity_load_single($target_type, $field_item['target_id']);
// Check if there is already a translation available for this
// entity. If so, just continue with the next one.
$handler = entity_translation_get_handler($target_type, $ref_entity);
if ($handler instanceof EntityTranslationHandlerInterface) {
$translations = $handler->getTranslations();
if (isset($translations->data[$job->target_language])) {
continue;
}
}
// Add suggestion.
$suggestions[] = array(
'job_item' => tmgmt_job_item_create('entity', $target_type, $field_item['target_id']),
'reason' => t('Field @label', array('@label' => $instance['label'])),
'from_item' => $item->tjiid,
);
}
}
break;
}
}
}
}
return $suggestions;
}

View File

@@ -0,0 +1,55 @@
<?php
/**
* Tests integration with pathauto.
*/
class TMGMTEntitySourcePathAutoTestCase extends TMGMTEntityTestCaseUtility {
static function getInfo() {
return array(
'name' => 'Entity Source Pathauto tests',
'description' => 'Verifies that the correct aliases are generated for entity transations',
'group' => 'Translation Management',
'dependencies' => array('entity_translation', 'pathauto'),
);
}
function setUp() {
parent::setUp(array('tmgmt_entity', 'entity_translation', 'pathauto'));
$this->loginAsAdmin();
$this->createNodeType('article', 'Article', ENTITY_TRANSLATION_ENABLED);
}
/**
* Tests that pathauto aliases are correctly created.
*/
function testAliasCreation() {
$this->setEnvironment('de');
// Create a translation job.
$job = $this->createJob();
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
// Create a node.
$node = $this->createNode('article');
// Create a job item for this node and add it to the job.
$job->addItem('entity', 'node', $node->nid);
// Translate the job.
$job->requestTranslation();
// Check the translated job items.
foreach ($job->getItems() as $item) {
$item->acceptTranslation();
}
// Make sure that the correct url aliases were created.
$aliases = db_query('SELECT * FROM {url_alias} where source = :source', array(':source' => 'node/' . $node->nid))->fetchAllAssoc('language');
$this->assertEqual(2, count($aliases));
$this->assertTrue(isset($aliases['en']), 'English alias created.');
$this->assertTrue(isset($aliases['de']), 'German alias created.');
}
}

View File

@@ -0,0 +1,110 @@
<?php
/**
* @file
* Provides the Entity source controller.
*/
class TMGMTEntitySourcePluginController extends TMGMTDefaultSourcePluginController {
public function getLabel(TMGMTJobItem $job_item) {
if ($entity = entity_load_single($job_item->item_type, $job_item->item_id)) {
return entity_label($job_item->item_type, $entity);
}
}
public function getUri(TMGMTJobItem $job_item) {
if ($entity = entity_load_single($job_item->item_type, $job_item->item_id)) {
return entity_uri($job_item->item_type, $entity);
}
}
/**
* {@inheritdoc}
*
* Returns the data from the fields as a structure that can be processed by
* the Translation Management system.
*/
public function getData(TMGMTJobItem $job_item) {
$entity = entity_load_single($job_item->item_type, $job_item->item_id);
if (!$entity) {
throw new TMGMTException(t('Unable to load entity %type with id %id', array('%type' => $job_item->item_type, $job_item->item_id)));
}
if (entity_language($job_item->item_type, $entity) == LANGUAGE_NONE) {
throw new TMGMTException(t('Entity %entity could not be translated because it is language neutral', array('%entity' => entity_label($job_item->item_type, $entity))));
}
return tmgmt_field_get_source_data($job_item->item_type, $entity, $job_item->getJob()->source_language, TRUE);
}
/**
* {@inheritdoc}
*/
public function saveTranslation(TMGMTJobItem $job_item) {
$entity = entity_load_single($job_item->item_type, $job_item->item_id);
$job = tmgmt_job_load($job_item->tjid);
tmgmt_field_populate_entity($job_item->item_type, $entity, $job->target_language, $job_item->getData());
// Change the active language of the entity to the target language.
$handler = entity_translation_get_handler($job_item->item_type, $entity);
$handler->setFormLanguage($job_item->getJob()->target_language);
entity_save($job_item->item_type, $entity);
$translation = array(
// @todo Improve hardcoded values.
'translate' => 0,
'status' => TRUE,
'language' => $job_item->getJob()->target_language,
'source' => $job_item->getJob()->source_language,
);
$handler->setTranslation($translation);
$handler->saveTranslations();
$job_item->accepted();
}
/**
* {@inheritdoc}
*/
public function getType(TMGMTJobItem $job_item) {
if ($entity = entity_load_single($job_item->item_type, $job_item->item_id)) {
$bundles = tmgmt_entity_get_translatable_bundles($job_item->item_type);
$info = entity_get_info($job_item->item_type);
list(, , $bundle) = entity_extract_ids($job_item->item_type, $entity);
// Display entity type and label if we have one and the bundle isn't
// the same as the entity type.
if (isset($bundles[$bundle]) && $bundle != $job_item->item_type) {
return t('@type (@bundle)', array('@type' => $info['label'], '@bundle' => $bundles[$bundle]));
}
// Otherwise just display the entity type label.
elseif (isset($info['label'])) {
return $info['label'];
}
return parent::getType($job_item);
}
}
/**
* {@inheritdoc}
*/
public function getSourceLangCode(TMGMTJobItem $job_item) {
$entity = entity_load_single($job_item->item_type, $job_item->item_id);
return isset($entity->translations->original) ? $entity->translations->original : NULL;
}
/**
* {@inheritdoc}
*/
public function getExistingLangCodes(TMGMTJobItem $job_item) {
if ($entity = entity_load_single($job_item->item_type, $job_item->item_id)) {
$entity_info = entity_get_info($job_item->item_type);
if (isset($entity_info['entity keys']['translations'])){
$translations_key = $entity_info['entity keys']['translations'];
return array_keys($entity->{$translations_key}->data);
}
}
return array();
}
}

View File

@@ -0,0 +1,101 @@
<?php
/**
* Entity source LANGUAGE_NONE tests.
*
* Splitted into a separate test because of https://www.drupal.org/node/2675230.
*/
class TMGMTEntitySourceLanguageNoneTestCase extends TMGMTEntityTestCaseUtility {
public $vocabulary;
static function getInfo() {
return array(
'name' => 'Entity Source Neutral tests',
'description' => 'Tests that LANGUAGE_NONE entities can not be translated',
'group' => 'Translation Management',
'dependencies' => array('entity_translation'),
);
}
function setUp() {
parent::setUp(array('tmgmt_entity', 'taxonomy', 'entity_translation'));
// Admin user to perform settings on setup.
$this->loginAsAdmin(array('administer entity translation'));
$this->vocabulary = $this->createTaxonomyVocab(strtolower($this->randomName()), $this->randomName(), array(FALSE, TRUE, TRUE, TRUE));
// Enable entity translations for taxonomy.
$edit['entity_translation_entity_types[taxonomy_term]'] = 1;
$this->drupalPost('admin/config/regional/entity_translation', $edit, t('Save configuration'));
}
/**
* Test if language neutral entities are not allowed for translation.
*
* That behaviour is described in the entity_translation documentation:
* https://www.drupal.org/node/1280934
*/
function testLanguageNeutral() {
$this->setEnvironment('de');
// Structure: array({entity-type} => array({source-langcode} => {entity}))
$test_data = array();
$this->createNodeType('article', 'Article', ENTITY_TRANSLATION_ENABLED);
$test_data['node'][LANGUAGE_NONE] = $this->createNode('article', LANGUAGE_NONE);
$test_data['node']['en'] = $this->createNode('article', 'en');
$test_data['node']['de'] = $this->createNode('article', 'de');
$test_data['taxonomy_term'][LANGUAGE_NONE] = $this->createTaxonomyTerm($this->vocabulary, LANGUAGE_NONE);
$test_data['taxonomy_term']['en'] = $this->createTaxonomyTerm($this->vocabulary, 'en');
$test_data['taxonomy_term']['de'] = $this->createTaxonomyTerm($this->vocabulary, 'de');
// Test if tmgmt_entity_get_translatable_entities() function excludes
// language neutral entities.
foreach ($test_data as $entity_type => $entities) {
$translatable_entities = tmgmt_entity_get_translatable_entities($entity_type);
foreach ($entities as $langcode => $entity) {
list($id, , ) = entity_extract_ids($entity_type, $entity);
if ($langcode == LANGUAGE_NONE) {
$this->assert(!isset($translatable_entities[$id]), "Language neutral $entity_type entity does not exist in the translatable entities list.");
}
else {
$this->assert(isset($translatable_entities[$id]), "$langcode $entity_type entity exists in the translatable entities list.");
}
}
}
// Test if language neutral entities can't be added to a translation job.
$job = $this->createJob();
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
foreach ($test_data as $entity_type => $entities) {
foreach ($entities as $langcode => $entity) {
list($id, , ) = entity_extract_ids($entity_type, $entity);
try {
$job->addItem('entity', $entity_type, $id);
if ($langcode == LANGUAGE_NONE) {
$this->fail("Adding of language neutral $entity_type entity to a translation job did not fail.");
}
else {
$this->pass("Adding of $langcode $entity_type entity node to a translation job did not fail.");
}
}
catch (TMGMTException $e) {
if ($langcode == LANGUAGE_NONE) {
$this->pass("Adding of language neutral $entity_type entity to a translation job did fail.");
}
else {
$this->fail("Adding of $langcode $entity_type entity node to a translation job did fail.");
}
}
}
}
$GLOBALS['TMGMT_DEBUG'] = FALSE;
}
}

View File

@@ -0,0 +1,212 @@
<?php
/**
* Basic Entity Source tests.
*/
class TMGMTEntitySourceTestCase extends TMGMTEntityTestCaseUtility {
public $vocabulary;
static function getInfo() {
return array(
'name' => 'Entity Source tests',
'description' => 'Exporting source data from entities and saving translations back to entities.',
'group' => 'Translation Management',
'dependencies' => array('entity_translation'),
);
}
function setUp() {
parent::setUp(array('tmgmt_entity', 'taxonomy', 'entity_translation'));
// Admin user to perform settings on setup.
$this->loginAsAdmin(array('administer entity translation'));
$this->vocabulary = $this->createTaxonomyVocab(strtolower($this->randomName()), $this->randomName(), array(FALSE, TRUE, TRUE, TRUE));
// Enable entity translations for taxonomy.
$edit['entity_translation_entity_types[taxonomy_term]'] = 1;
$this->drupalPost('admin/config/regional/entity_translation', $edit, t('Save configuration'));
}
/**
* Tests nodes field translation.
*/
function testEntitySourceNode() {
$this->setEnvironment('de');
$this->createNodeType('article', 'Article', ENTITY_TRANSLATION_ENABLED);
// Create a translation job.
$job = $this->createJob();
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
// Create some nodes.
for ($i = 1; $i <= 5; $i++) {
$node = $this->createNode('article');
// Create a job item for this node and add it to the job.
$item = $job->addItem('entity', 'node', $node->nid);
$this->assertEqual(t('@type (@bundle)', array('@type' => t('Node'), '@bundle' => 'Article')), $item->getSourceType());
}
// Translate the job.
$job->requestTranslation();
// Check the translated job items.
foreach ($job->getItems() as $item) {
// The source is available only for en.
$this->assertJobItemLangCodes($item, 'en', array('en'));
$item->acceptTranslation();
$this->assertTrue($item->isAccepted());
$entity = entity_load_single($item->item_type, $item->item_id);
$data = $item->getData();
$this->checkTranslatedData($entity, $data, 'de');
$this->checkUntranslatedData($entity, $this->field_names['node']['article'], $data, 'de');
// The source is now available for both en and de.
$this->assertJobItemLangCodes($item, 'en', array('de', 'en'));
}
}
/**
* Tests taxonomy terms field translation.
*/
function testEntitySourceTerm() {
$this->setEnvironment('de');
// Create the job.
$job = $this->createJob();
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
$term = NULL;
//Create some terms.
for ($i = 1; $i <= 5; $i++) {
$term = $this->createTaxonomyTerm($this->vocabulary);
// Create the item and assign it to the job.
$item = $job->addItem('entity', 'taxonomy_term', $term->tid);
$this->assertEqual(t('@type (@bundle)', array('@type' => t('Taxonomy term'), '@bundle' => $this->vocabulary->name)), $item->getSourceType());
}
// Request the translation and accept it.
$job->requestTranslation();
// Check if the fields were translated.
foreach ($job->getItems() as $item) {
$this->assertJobItemLangCodes($item, 'en', array('en'));
$item->acceptTranslation();
$entity = entity_load_single($item->item_type, $item->item_id);
$data = $item->getData();
$this->checkTranslatedData($entity, $data, 'de');
$this->checkUntranslatedData($entity, $this->field_names['taxonomy_term'][$this->vocabulary->machine_name], $data, 'de');
$this->assertJobItemLangCodes($item, 'en', array('de', 'en'));
}
}
function testAddingJobItemsWithEmptySourceText() {
$this->setEnvironment('de');
// Create term with empty texts.
$empty_term = new stdClass();
$empty_term->name = $this->randomName();
$empty_term->description = $this->randomName();
$empty_term->vid = $this->vocabulary->vid;
taxonomy_term_save($empty_term);
// Create the job.
$job = tmgmt_job_create('en', NULL);
try {
$job->addItem('entity', 'taxonomy_term', $empty_term->tid);
$this->fail('Job item added with empty source text.');
}
catch (TMGMTException $e) {
$this->assert(empty($job->tjid), 'After adding a job item with empty source text its tjid has to be unset.');
}
// Create term with populated source content.
$populated_content_term = $this->createTaxonomyTerm($this->vocabulary);
// Lets reuse the last created term with populated source content.
$job->addItem('entity', 'taxonomy_term', $populated_content_term->tid);
$this->assert(!empty($job->tjid), 'After adding another job item with populated source text its tjid must be set.');
}
/**
* Test if the source is able to pull content in requested language.
*/
function testRequestDataForSpecificLanguage() {
$this->setEnvironment('de');
$this->setEnvironment('cs');
$this->createNodeType('article', 'Article', ENTITY_TRANSLATION_ENABLED);
// Create a translation job.
$job = $this->createJob('en', 'de');
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
$node = $this->createNode('article', 'cs');
$node->body['en'][0]['value'] = 'en translation';
node_save($node);
$job->addItem('entity', 'node', $node->nid);
$data = $job->getData();
$this->assertEqual($data[1]['body'][0]['value']['#text'], 'en translation');
}
/**
* Compares the data from an entity with the translated data.
*
* @param $tentity
* The translated entity object.
* @param $data
* An array with the translated data.
* @param $langcode
* The code of the target language.
*/
function checkTranslatedData($tentity, $data, $langcode) {
foreach (element_children($data) as $field_name) {
foreach (element_children($data[$field_name]) as $delta) {
foreach (element_children($data[$field_name][$delta]) as $column) {
$column_value = $data[$field_name][$delta][$column];
if (!empty($column_value['#translate'])) {
$this->assertEqual($tentity->{$field_name}[$langcode][$delta][$column], $column_value['#translation']['#text'], format_string('The field %field:%delta has been populated with the proper translated data.', array('%field' => $field_name, 'delta' => $delta)));
}
else {
$this->assertEqual($tentity->{$field_name}[$langcode][$delta][$column], $column_value['#text'], format_string('The field %field:%delta has been populated with the proper untranslated data.', array('%field' => $field_name, 'delta' => $delta)));
}
}
}
}
}
/**
* Checks the fields that should not be translated.
*
* @param $tentity
* The translated entity object.
* @param $fields
* An array with the field names to check.
* @param $translation
* An array with the translated data.
* @param $langcode
* The code of the target language.
*/
function checkUntranslatedData($tentity, $fields, $data, $langcode) {
foreach ($fields as $field_name) {
$field_info = field_info_field($field_name);
if (!$field_info['translatable']) {
// Avoid some PHP warnings.
if (isset($data[$field_name])) {
$this->assertNull($data[$field_name]['#translation']['#text'], 'The not translatable field was not translated.');
}
if (isset($tentity->{$field_name}[$langcode])) {
$this->assertNull($tentity->{$field_name}[$langcode], 'The entity has translated data in a field that is translatable.');
}
}
}
}
}

View File

@@ -0,0 +1,274 @@
<?php
/*
* @file
* Contains tests for Translation management
*/
/**
* Basic Source-Suggestions tests.
*/
class TMGMTSuggestionsTestCase extends TMGMTBaseTestCase {
static function getInfo() {
return array(
'name' => 'Entity Suggestions tests',
'description' => 'Tests suggestion implementation for the entity source plugin',
'group' => 'Translation Management',
'dependencies' => array(
'file_entity',
'entityreference',
),
);
}
public function setUp() {
parent::setUp(array('file_entity', 'tmgmt_entity', 'tmgmt_ui', 'entityreference', 'tmgmt_i18n_string', 'i18n_menu'));
$this->loginAsAdmin(array('administer entity translation'));
$this->setEnvironment('de');
// Enable entity translations for nodes and comments.
$edit = array();
$edit['entity_translation_entity_types[node]'] = 1;
$edit['entity_translation_entity_types[file]'] = 1;
$this->drupalPost('admin/config/regional/entity_translation', $edit, t('Save configuration'));
}
/**
* Prepare a node to get suggestions from.
*
* Creates a node with two file fields. The first one is not translatable,
* the second one is. Both fields got two files attached, where one has
* translatable content (title and atl-text) and the other one not.
*
* @return object
* The node which is prepared with all needed fields for the suggestions.
*/
protected function prepareTranslationSuggestions() {
// Create a content type with fields.
// Only the first field is a translatable reference.
$type = $this->drupalCreateContentType();
$field1 = field_create_field(array(
'field_name' => 'field1',
'type' => 'file',
'cardinality' => -1,
));
$field2 = field_create_field(array(
'field_name' => 'field2',
'type' => 'file',
'cardinality' => -1,
'translatable' => TRUE,
));
$field3 = field_create_field(array(
'field_name' => 'field3',
'type' => 'entityreference',
'cardinality' => -1,
'settings' => array(
'target_type' => 'node',
'handler' => 'base',
'handler_settings' => array(
'target_bundles' => array($type->type => $type->type),
'sort' => array('type' => 'none'),
),
),
));
// Create field instances on the content type.
field_create_instance(array(
'field_name' => $field1['field_name'],
'entity_type' => 'node',
'bundle' => $type->type,
'label' => 'Field 1',
'widget' => array('type' => 'file'),
'settings' => array(),
));
field_create_instance(array(
'field_name' => $field2['field_name'],
'entity_type' => 'node',
'bundle' => $type->type,
'label' => 'Field 2',
'widget' => array('type' => 'file'),
'settings' => array(),
));
field_create_instance(array(
'field_name' => $field3['field_name'],
'entity_type' => 'node',
'bundle' => $type->type,
'label' => 'Field 3',
'settings' => array(),
'widget' => array('type' => 'entityreference_autocomplete_tags'),
));
// Make the body field translatable from node.
$info = field_info_field('body');
$info['translatable'] = TRUE;
field_update_field($info);
// Make the file entity fields translatable.
$info = field_info_field('field_file_image_alt_text');
$info['translatable'] = TRUE;
field_update_field($info);
$info = field_info_field('field_file_image_title_text');
$info['translatable'] = TRUE;
field_update_field($info);
// Create and save files - two with some text and two with no text.
list($file1, $file2, $file3, $file4) = $this->drupalGetTestFiles('image');
$file2->field_file_image_alt_text['en'][0] = array(
'value' => $this->randomName(),
'type' => 'plain_text',
);
$file2->field_file_image_title_text['en'][0] = array(
'value' => $this->randomName() . ' ' . $this->randomName(),
'type' => 'plain_text',
);
$file4->field_file_image_alt_text['en'][0] = array(
'value' => $this->randomName(),
'type' => 'plain_text',
);
$file4->field_file_image_title_text['en'][0] = array(
'value' => $this->randomName() . ' ' . $this->randomName(),
'type' => 'plain_text',
);
file_save($file1);
file_save($file2);
file_save($file3);
file_save($file4);
// Create a dummy node that will be referenced
$referenced_node = $this->drupalCreateNode(array(
'type' => $type->type,
'language' => 'en',
'body' => array(
'en' => array(
array('value' => $this->randomName() . ' ' . $this->randomName()),
),
),
));
// Create a node with two translatable and two non-translatable files.
$node = $this->drupalCreateNode(array(
'type' => $type->type,
'language' => 'en',
'body' => array('en' => array(
array(
'value' => $this->randomName(),
),
)),
$field1['field_name'] => array(LANGUAGE_NONE => array(
array(
'fid' => $file1->fid,
'display' => 1,
'description' => '',
),
array(
'fid' => $file2->fid,
'display' => 1,
'description' => '',
),
)),
$field2['field_name'] => array(LANGUAGE_NONE => array(
array(
'fid' => $file3->fid,
'display' => 1,
'description' => '',
),
array(
'fid' => $file4->fid,
'display' => 1,
'description' => '',
),
)),
$field3['field_name'] => array(LANGUAGE_NONE => array(
array('target_id' => $referenced_node->nid),
)),
));
// Create a translatable menu.
$config = array(
'menu_name' => 'translatable-menu',
'title' => 'Translatable menu',
'description' => $this->randomName(),
'i18n_mode' => I18N_MODE_MULTIPLE,
);
menu_save($config);
$menu = menu_load($config['menu_name']);
// Create a menu link for the node.
$menu_link = array(
'link_path' => 'node/' . $node->nid,
'link_title' => 'Menu link one',
// i18n_menu_link::get_title() uses the title, set that too.
'title' => 'Menu link one',
'menu_name' => $menu['menu_name'],
'customized' => TRUE,
);
$node->link = menu_link_load(menu_link_save($menu_link));
return $node;
}
/**
* Test suggested entities from a translation job.
*/
public function testSuggestions() {
// Prepare a job and a node for testing.
$job = $this->createJob();
$node = $this->prepareTranslationSuggestions();
$item = $job->addItem('entity', 'node', $node->nid);
// Get all suggestions and clean the list.
$suggestions = $job->getSuggestions();
$job->cleanSuggestionsList($suggestions);
// Check for suggestions.
$this->assertEqual(count($suggestions), 4, 'Found four suggestions.');
// Check for valid attributes on the suggestions.
foreach ($suggestions as $suggestion) {
switch ($suggestion['reason']) {
case 'Field Field 1':
$this->assertEqual($suggestion['job_item']->getWordCount(), 3, 'Three translatable words in the suggestion.');
$this->assertEqual($suggestion['job_item']->plugin, 'entity', 'Got an entity as plugin in the suggestion.');
$this->assertEqual($suggestion['job_item']->item_type, 'file', 'Got a file in the suggestion.');
$this->assertEqual($suggestion['job_item']->item_id, $node->field1[LANGUAGE_NONE][1]['fid'], 'File id match between node and suggestion.');
break;
case 'Field Field 2':
$this->assertEqual($suggestion['job_item']->getWordCount(), 3, 'Three translatable words in the suggestion.');
$this->assertEqual($suggestion['job_item']->plugin, 'entity', 'Got an entity as plugin in the suggestion.');
$this->assertEqual($suggestion['job_item']->item_type, 'file', 'Got a file in the suggestion.');
$this->assertEqual($suggestion['job_item']->item_id, $node->field2[LANGUAGE_NONE][1]['fid'], 'File id match between node and suggestion.');
break;
case 'Field Field 3':
$this->assertEqual($suggestion['job_item']->getWordCount(), 2, 'Two translatable words in the suggestion');
$this->assertEqual($suggestion['job_item']->plugin, 'entity', 'Got an entity as plugin in the suggestion.');
$this->assertEqual($suggestion['job_item']->item_type, 'node', 'Got a node in the suggestion.');
$this->assertEqual($suggestion['job_item']->item_id, $node->field3[LANGUAGE_NONE][0]['target_id'], 'File id match between node and suggestion.');
break;
case 'Menu link Menu link one':
$this->assertEqual($suggestion['job_item']->getWordCount(), 3, 'Three translatable words in the suggestion' . $suggestion['job_item']->plugin . $suggestion['job_item']->item_type);
$this->assertEqual($suggestion['job_item']->plugin, 'i18n_string', 'Got a string as plugin in the suggestion.');
$this->assertEqual($suggestion['job_item']->item_type, 'menu_link', 'Got a menu link in the suggestion.');
$this->assertEqual($suggestion['job_item']->item_id, 'menu:item:'. $node->link['mlid'], 'Menu link id match between menu link and suggestion.');
break;
default:
$this->fail('Found an invalid suggestion.');
break;
}
$this->assertEqual($suggestion['from_item'], $item->tjiid);
$job->addExistingItem($suggestion['job_item']);
}
// Re-get all suggestions.
$suggestions = $job->getSuggestions();
$job->cleanSuggestionsList($suggestions);
// Check for no more suggestions.
$this->assertEqual(count($suggestions), 0, 'Found no more suggestion.');
}
}

View File

@@ -0,0 +1,257 @@
<?php
/**
* Abstract entity ui controller class for source plugin that provides
* getEntity() method to retrieve list of entities of specific type. It also
* allows to implement alter hook to alter the entity query for a specific type.
*
* @ingroup tmgmt_source
*/
abstract class TMGMTEntityDefaultSourceUIController extends TMGMTDefaultSourceUIController {
/**
* Entity source list items limit.
*
* @var int
*/
public $pagerLimit = 25;
/**
* Gets entities data of provided type needed to build overview form list.
*
* @param $type
* Entity type for which to get list of entities.
* @param array $property_conditions
* Array of key => $value pairs passed into
* tmgmt_entity_get_translatable_entities() as the second parameter.
*
* @return array
* Array of entities.
*/
public function getEntitiesTranslationData($type, $property_conditions = array()) {
$return_value = array();
$entities = tmgmt_entity_get_translatable_entities($type, $property_conditions, TRUE);
$entity_info = entity_get_info($type);
$bundles = tmgmt_entity_get_translatable_bundles($type);
// For retrieved entities add translation specific data.
foreach ($entities as $entity) {
list($entity_id, , $bundle) = entity_extract_ids($type, $entity);
$entity_uri = entity_uri($type, $entity);
// This occurs on user entity type.
if (empty($entity_id)) {
continue;
}
/**
* @var EntityTranslationDefaultHandler $handler
*/
$handler = entity_translation_get_handler($type, $entity);
// Get existing translations and current job items for the entity
// to determine translation statuses
$translations = $handler->getTranslations();
$source_lang = entity_language($type, $entity);
$current_job_items = tmgmt_job_item_load_latest('entity', $type, $entity_id, $source_lang);
// Load basic entity data.
$return_value[$entity_id] = array(
'entity_type' => $type,
'entity_id' => $entity_id,
'entity_label' => entity_label($type, $entity),
'entity_uri' => $entity_uri['path'],
);
if (count($bundles) > 1) {
$return_value[$entity_id]['bundle'] = isset($bundles[$bundle]) ? $bundles[$bundle] : t('Unknown');
}
// Load entity translation specific data.
foreach (language_list() as $langcode => $language) {
$translation_status = 'current';
if ($langcode == $source_lang) {
$translation_status = 'original';
}
elseif (!isset($translations->data[$langcode])) {
$translation_status = 'missing';
}
elseif (!empty($translations->data[$langcode]['translate'])) {
$translation_status = 'outofdate';
}
$return_value[$entity_id]['current_job_items'][$langcode] = isset($current_job_items[$langcode]) ? $current_job_items[$langcode]: NULL;
$return_value[$entity_id]['translation_statuses'][$langcode] = $translation_status;
}
}
return $return_value;
}
/**
* Builds search form for entity sources overview.
*
* @param array $form
* Drupal form array.
* @param $form_state
* Drupal form_state array.
* @param $type
* Entity type.
*
* @return array
* Drupal form array.
*/
public function overviewSearchFormPart($form, &$form_state, $type) {
// Add search form specific styling.
drupal_add_css(drupal_get_path('module', 'tmgmt_entity') . '/css/tmgmt_entity.admin.entity_source_search_form.css');
$form = array();
// Add entity type value into form array so that it is available in
// the form alter hook.
$form_state['entity_type'] = $type;
$form['search_wrapper'] = array(
'#prefix' => '<div class="tmgmt-sources-wrapper tmgmt-entity-sources-wrapper">',
'#suffix' => '</div>',
'#weight' => -15,
);
$form['search_wrapper']['search'] = array(
'#tree' => TRUE,
);
$form['search_wrapper']['search_submit'] = array(
'#type' => 'submit',
'#value' => t('Search'),
'#weight' => 10,
);
$form['search_wrapper']['search_cancel'] = array(
'#type' => 'submit',
'#value' => t('Cancel'),
'#weight' => 11,
);
$entity_info = entity_get_info($type);
$label_key = isset($entity_info['entity keys']['label']) ? $entity_info['entity keys']['label'] : NULL;
if (!empty($label_key)) {
$form['search_wrapper']['search'][$label_key] = array(
'#type' => 'textfield',
'#title' => t('@entity_name title', array('@entity_name' => $entity_info['label'])),
'#size' => 25,
'#default_value' => isset($_GET[$label_key]) ? $_GET[$label_key] : NULL,
);
}
$language_options = array();
foreach (language_list() as $langcode => $language) {
$language_options[$langcode] = $language->name;
}
$form['search_wrapper']['search']['language'] = array(
'#type' => 'select',
'#title' => t('Source Language'),
'#options' => $language_options,
'#empty_option' => t('All'),
'#default_value' => isset($_GET['language']) ? $_GET['language'] : NULL,
);
$bundle_key = $entity_info['entity keys']['bundle'];
$bundle_options = tmgmt_entity_get_translatable_bundles($type);
if (count($bundle_options) > 1) {
$form['search_wrapper']['search'][$bundle_key] = array(
'#type' => 'select',
'#title' => t('@entity_name type', array('@entity_name' => $entity_info['label'])),
'#options' => $bundle_options,
'#empty_option' => t('All'),
'#default_value' => isset($_GET[$bundle_key]) ? $_GET[$bundle_key] : NULL,
);
}
// In case entity translation is not enabled for any of bundles
// display appropriate message.
elseif (count($bundle_options) == 0) {
drupal_set_message(t('Entity translation is not enabled for any of existing content types. To use this functionality go to Content types administration and enable entity translation for desired content types.'), 'warning');
unset($form['search_wrapper']);
}
$options = array();
foreach (language_list() as $langcode => $language) {
$options[$langcode] = $language->name;
}
$form['search_wrapper']['search']['target_language'] = array(
'#type' => 'select',
'#title' => t('Target language'),
'#options' => $options,
'#empty_option' => t('Any'),
'#default_value' => isset($_GET['target_language']) ? $_GET['target_language'] : NULL,
);
$form['search_wrapper']['search']['target_status'] = array(
'#type' => 'select',
'#title' => t('Target status'),
'#options' => array(
'untranslated_or_outdated' => t('Untranslated or outdated'),
'untranslated' => t('Untranslated'),
'outdated' => t('Outdated'),
),
'#default_value' => isset($_GET['target_status']) ? $_GET['target_status'] : NULL,
'#states' => array(
'invisible' => array(
':input[name="search[target_language]"]' => array('value' => ''),
),
),
);
return $form;
}
/**
* Performs redirect with search params appended to the uri.
*
* In case of triggering element is edit-search-submit it redirects to
* current location with added query string containing submitted search form
* values.
*
* @param array $form
* Drupal form array.
* @param $form_state
* Drupal form_state array.
* @param $type
* Entity type.
*/
public function overviewSearchFormRedirect($form, &$form_state, $type) {
if ($form_state['triggering_element']['#id'] == 'edit-search-cancel') {
drupal_goto($_GET['q']);
}
elseif ($form_state['triggering_element']['#id'] == 'edit-search-submit') {
$query = array();
foreach ($form_state['values']['search'] as $key => $value) {
$query[$key] = $value;
}
drupal_goto($_GET['q'], array('query' => $query));
}
}
/**
* {@inheritdoc}
*/
public function hook_menu() {
$items = parent::hook_menu();
if (isset($items['admin/tmgmt/sources/entity_node'])) {
// We assume that nodes are the most important overview if enabled, so
// make sure they show up first.
$items['admin/tmgmt/sources/entity_node']['weight'] = -20;
}
return $items;
}
}

View File

@@ -0,0 +1,20 @@
name = Entity Source User Interface
description = User Interface for the entity translation source plugin.
package = Translation Management
core = 7.x
dependencies[] = tmgmt_entity
dependencies[] = tmgmt_ui
dependencies[] = views_bulk_operations
files[] = tmgmt_entity_ui.test
files[] = tmgmt_entity_ui.list.test
files[] = tmgmt_entity_ui.ui.inc
; Information added by Drupal.org packaging script on 2016-09-21
version = "7.x-1.0-rc2+1-dev"
core = "7.x"
project = "tmgmt"
datestamp = "1474446494"

View File

@@ -0,0 +1,268 @@
<?php
class TMGMTEntitySourceListTestCase extends TMGMTEntityTestCaseUtility {
protected $nodes = array();
static function getInfo() {
return array(
'name' => 'Entity Source List tests',
'description' => 'Tests the user interface for entity translation lists.',
'group' => 'Translation Management',
);
}
function setUp() {
parent::setUp(array('tmgmt_entity_ui', 'translation', 'comment', 'taxonomy'));
$this->loginAsAdmin(array('administer entity translation'));
$this->setEnvironment('de');
$this->setEnvironment('fr');
// Enable entity translations for nodes and comments.
$edit = array();
$edit['entity_translation_entity_types[comment]'] = 1;
$edit['entity_translation_entity_types[node]'] = 1;
$edit['entity_translation_entity_types[taxonomy_term]'] = 1;
$this->drupalPost('admin/config/regional/entity_translation', $edit, t('Save configuration'));
$this->createNodeType('article', 'Article', ENTITY_TRANSLATION_ENABLED);
$this->createNodeType('page', 'Page', TRANSLATION_ENABLED);
// Create nodes that will be used during tests.
// NOTE that the order matters as results are read by xpath based on
// position in the list.
$this->nodes['page']['en'][] = $this->createNode('page');
$this->nodes['article']['de'][0] = $this->createNode('article', 'de');
$this->nodes['article']['fr'][0] = $this->createNode('article', 'fr');
$this->nodes['article']['en'][3] = $this->createNode('article', 'en');
$this->nodes['article']['en'][2] = $this->createNode('article', 'en');
$this->nodes['article']['en'][1] = $this->createNode('article', 'en');
$this->nodes['article']['en'][0] = $this->createNode('article', 'en');
}
/**
* Tests that the term bundle filter works.
*/
function testTermBundleFilter() {
$vocabulary1 = entity_create('taxonomy_vocabulary', array(
'machine_name' => 'vocab1',
'name' => $this->randomName(),
));
taxonomy_vocabulary_save($vocabulary1);
$term1 = entity_create('taxonomy_term', array(
'name' => $this->randomName(),
'vid' => $vocabulary1->vid,
));
taxonomy_term_save($term1);
$vocabulary2 = (object) array(
'machine_name' => 'vocab2',
'name' => $this->randomName(),
);
taxonomy_vocabulary_save($vocabulary2);
$term2 = entity_create('taxonomy_term', array(
'name' => $this->randomName(),
'vid' => $vocabulary2->vid,
));
taxonomy_term_save($term2);
$this->drupalGet('admin/tmgmt/sources/entity_taxonomy_term');
// Both terms should be displayed with their bundle.
$this->assertText($term1->name);
$this->assertText($term2->name);
$this->assertTrue($this->xpath('//td[text()=@vocabulary]', array('@vocabulary' => $vocabulary1->name)));
$this->assertTrue($this->xpath('//td[text()=@vocabulary]', array('@vocabulary' => $vocabulary2->name)));
// Limit to the first vocabulary.
$edit = array();
$edit['search[vocabulary_machine_name]'] = $vocabulary1->machine_name;
$this->drupalPost(NULL, $edit, t('Search'));
// Only term 1 should be displayed now.
$this->assertText($term1->name);
$this->assertNoText($term2->name);
$this->assertTrue($this->xpath('//td[text()=@vocabulary]', array('@vocabulary' => $vocabulary1->name)));
$this->assertFalse($this->xpath('//td[text()=@vocabulary]', array('@vocabulary' => $vocabulary2->name)));
}
function testAvailabilityOfEntityLists() {
$this->drupalGet('admin/tmgmt/sources/entity_comment');
// Check if we are at comments page.
$this->assertText(t('Comment overview (Entity)'));
// No comments yet - empty message is expected.
$this->assertText(t('No entities matching given criteria have been found.'));
$this->drupalGet('admin/tmgmt/sources/entity_node');
// Check if we are at nodes page.
$this->assertText(t('Node overview (Entity)'));
// We expect article title as article node type is entity translatable.
$this->assertText($this->nodes['article']['en'][0]->title);
// Page node type should not be listed as it is not entity translatable.
$this->assertNoText($this->nodes['page']['en'][0]->title);
}
function testTranslationStatuses() {
// Test statuses: Source, Missing.
$this->drupalGet('admin/tmgmt/sources/entity_node');
$langstatus_en = $this->xpath('//table[@id="tmgmt-entities-list"]/tbody/tr[1]/td[@class="langstatus-en"]');
$langstatus_de = $this->xpath('//table[@id="tmgmt-entities-list"]/tbody/tr[1]/td[@class="langstatus-de"]');
$this->assertEqual($langstatus_en[0]->div['title'], t('Source language'));
$this->assertEqual($langstatus_de[0]->div['title'], t('Not translated'));
// Test status: Active job item.
$job = $this->createJob('en', 'de');
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
$job->addItem('entity', 'node', $this->nodes['article']['en'][0]->nid);
$job->requestTranslation();
$this->drupalGet('admin/tmgmt/sources/entity_node');
$langstatus_de = $this->xpath('//table[@id="tmgmt-entities-list"]/tbody/tr[1]/td[@class="langstatus-de"]/a');
$items = $job->getItems();
$wrapper = entity_metadata_wrapper('tmgmt_job_item', array_shift($items));
$label = t('Active job item: @state', array('@state' => $wrapper->state->label()));
$this->assertEqual($langstatus_de[0]->div['title'], $label);
// Test status: Current
foreach ($job->getItems() as $job_item) {
$job_item->acceptTranslation();
}
$this->drupalGet('admin/tmgmt/sources/entity_node');
$langstatus_de = $this->xpath('//table[@id="tmgmt-entities-list"]/tbody/tr[1]/td[@class="langstatus-de"]');
$this->assertEqual($langstatus_de[0]->div['title'], t('Translation up to date'));
}
function testTranslationSubmissions() {
// Simple submission.
$nid = $this->nodes['article']['en'][0]->nid;
$edit = array();
$edit["items[$nid]"] = 1;
$this->drupalPost('admin/tmgmt/sources/entity_node', $edit, t('Request translation'));
$this->assertText(t('One job needs to be checked out.'));
// Submission of two entities of the same source language.
$nid1 = $this->nodes['article']['en'][0]->nid;
$nid2 = $this->nodes['article']['en'][1]->nid;
$edit = array();
$edit["items[$nid1]"] = 1;
$edit["items[$nid2]"] = 1;
$this->drupalPost('admin/tmgmt/sources/entity_node', $edit, t('Request translation'));
$this->assertText(t('One job needs to be checked out.'));
// Submission of several entities of different source languages.
$nid1 = $this->nodes['article']['en'][0]->nid;
$nid2 = $this->nodes['article']['en'][1]->nid;
$nid3 = $this->nodes['article']['en'][2]->nid;
$nid4 = $this->nodes['article']['en'][3]->nid;
$nid5 = $this->nodes['article']['de'][0]->nid;
$nid6 = $this->nodes['article']['fr'][0]->nid;
$edit = array();
$edit["items[$nid1]"] = 1;
$edit["items[$nid2]"] = 1;
$edit["items[$nid3]"] = 1;
$edit["items[$nid4]"] = 1;
$edit["items[$nid5]"] = 1;
$edit["items[$nid6]"] = 1;
$this->drupalPost('admin/tmgmt/sources/entity_node', $edit, t('Request translation'));
$this->assertText(t('@count jobs need to be checked out.', array('@count' => '3')));
}
function testNodeEntityListings() {
// Turn off the entity translation.
$edit = array();
$edit['language_content_type'] = TRANSLATION_ENABLED;
$this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type'));
// Check if we have appropriate message in case there are no entity
// translatable content types.
$this->drupalGet('admin/tmgmt/sources/entity_node');
$this->assertText(t('Entity translation is not enabled for any of existing content types. To use this functionality go to Content types administration and enable entity translation for desired content types.'));
// Turn on the entity translation for both - article and page - to test
// search form.
$edit = array();
$edit['language_content_type'] = ENTITY_TRANSLATION_ENABLED;
$this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type'));
$this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type'));
// Create page node after entity translation is enabled.
$page_node_translatable = $this->createNode('page');
$this->drupalGet('admin/tmgmt/sources/entity_node');
// We have both listed - one of articles and page.
$this->assertText($this->nodes['article']['en'][0]->title);
$this->assertText($page_node_translatable->title);
// Try the search by content type.
$edit = array();
$edit['search[type]'] = 'article';
$this->drupalPost('admin/tmgmt/sources/entity_node', $edit, t('Search'));
// There should be article present.
$this->assertText($this->nodes['article']['en'][0]->title);
// The page node should not be listed.
$this->assertNoText($page_node_translatable->title);
// Try cancel button - despite we do post content type search value
// we should get nodes of botch content types.
$this->drupalPost('admin/tmgmt/sources/entity_node', $edit, t('Cancel'));
$this->assertText($this->nodes['article']['en'][0]->title);
$this->assertText($page_node_translatable->title);
}
function testEntitySourceListSearch() {
// We need a node with title composed of several words to test
// "any words" search.
$title_part_1 = $this->randomName('4');
$title_part_2 = $this->randomName('4');
$title_part_3 = $this->randomName('4');
$this->nodes['article']['en'][0]->title = "$title_part_1 $title_part_2 $title_part_3";
node_save($this->nodes['article']['en'][0]);
// Submit partial node title and see if we have a result.
$edit = array();
$edit['search[title]'] = "$title_part_1 $title_part_3";
$this->drupalPost('admin/tmgmt/sources/entity_node', $edit, t('Search'));
$this->assertText("$title_part_1 $title_part_2 $title_part_3", 'Searching on partial node title must return the result.');
// Check if there is only one result in the list.
$search_result_rows = $this->xpath('//table[@id="tmgmt-entities-list"]/tbody/tr');
$this->assert(count($search_result_rows) == 1, 'The search result must return only one row.');
// To test if other entity types work go for simple comment search.
$comment = new stdClass();
$comment->comment_body[LANGUAGE_NONE][0]['value'] = $this->randomName();
$comment->subject = $this->randomName();
// We need to associate the comment with entity translatable node object.
$comment->nid = $this->nodes['article']['en'][0]->nid;
// Set defaults - without these we will get Undefined property notices.
$comment->is_anonymous = TRUE;
$comment->cid = 0;
$comment->pid = 0;
$comment->uid = 0;
// Will add further comment variables.
$comment = comment_submit($comment);
comment_save($comment);
// Do search for the comment.
$edit = array();
$edit['search[subject]'] = $comment->subject;
$this->drupalPost('admin/tmgmt/sources/entity_comment', $edit, t('Search'));
$this->assertText($comment->subject, 'Searching for a comment subject.');
}
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* @file
* Main module file for the translation management entity source plugin user
* interface.
*/
/**
* Implements hook_page_alter().
*/
function tmgmt_entity_ui_page_alter(&$page) {
if (entity_access('create', 'tmgmt_job')) {
// Translation tabs for nodes.
if (isset($page['content']['system_main']['entity_translation_overview'])) {
module_load_include('inc', 'tmgmt_entity_ui', 'tmgmt_entity_ui.pages');
$page['content']['system_main']['entity_translation_overview'] = drupal_get_form('tmgmt_entity_ui_translate_form', $page['content']['system_main']);
}
// Support the context module: when context is used for placing the
// system_main block, then block contents appear nested one level deeper
// under another 'content' key.
elseif (isset($page['content']['system_main']['content']['entity_translation_overview'])) {
module_load_include('inc', 'tmgmt_entity_ui', 'tmgmt_entity_ui.pages');
$page['content']['system_main']['content']['entity_translation_overview'] = drupal_get_form('tmgmt_entity_ui_translate_form', $page['content']['system_main']['content']);
}
}
}
/**
* Implements tmgmt_entity_tmgmt_source_plugin_info_alter().
*/
function tmgmt_entity_ui_tmgmt_source_plugin_info_alter(&$info) {
// Define ui controller class to handle Drupal entities.
$info['entity']['ui controller class'] = 'TMGMTEntitySourceUIController';
// Alter file and file path info so that tmgmt_entity_ui module is targeted
// for page callback.
$info['entity']['file'] = 'tmgmt_entity_ui.pages.inc';
$info['entity']['file path'] = drupal_get_path('module', 'tmgmt_entity_ui');
}

View File

@@ -0,0 +1,135 @@
<?php
/**
* @file
* Provides page and form callbacks for the Translation Management Tool Entity
* Source User Interface module.
*/
/**
* Entity translation overview form.
*/
function tmgmt_entity_ui_translate_form($form, &$form_state, $build) {
// Store the entity in the form state so we can easily create the job in the
// submit handler.
$form_state['entity_type'] = $build['#entity_type'];
$form_state['entity'] = $build['#entity'];
$overview = $build['entity_translation_overview'];
list($id, $vid, $bundle) = entity_extract_ids($form_state['entity_type'], $form_state['entity']);
$form['top_actions']['#type'] = 'actions';
$form['top_actions']['#weight'] = -10;
tmgmt_ui_add_cart_form($form['top_actions'], $form_state, 'entity', $build['#entity_type'], $id);
// Inject our additional column into the header.
array_splice($overview['#header'], -1, 0, array(t('Pending Translations')));
// Make this a tableselect form.
$form['languages'] = array(
'#type' => 'tableselect',
'#header' => $overview['#header'],
'#options' => array(),
);
$languages = language_list();
// Check if there is a job / job item that references this translation.
$entity_language = entity_language($form_state['entity_type'], $form_state['entity']);
$items = tmgmt_job_item_load_latest('entity', $form_state['entity_type'], $id, $entity_language);
foreach ($languages as $langcode => $language) {
if ($langcode == LANGUAGE_NONE) {
// Never show language neutral on the overview.
continue;
}
// Since the keys are numeric and in the same order we can shift one element
// after the other from the original non-form rows.
$option = array_shift($overview['#rows']);
if ($langcode == $entity_language) {
$additional = '<strong>' . t('Source') . '</strong>';
// This is the source object so we disable the checkbox for this row.
$form['languages'][$langcode] = array(
'#type' => 'checkbox',
'#disabled' => TRUE,
);
}
elseif (isset($items[$langcode])) {
$item = $items[$langcode];
$uri = $item->uri();
$wrapper = entity_metadata_wrapper('tmgmt_job_item', $item);
$additional = l($wrapper->state->label(), $uri['path'], array('query' => array('destination' => current_path())));
// Disable the checkbox for this row since there is already a translation
// in progress that has not yet been finished. This way we make sure that
// we don't stack multiple active translations for the same item on top
// of each other.
$form['languages'][$langcode] = array(
'#type' => 'checkbox',
'#disabled' => TRUE,
);
}
else {
// There is no translation job / job item for this target language.
$additional = t('None');
}
// Inject the additional column into the array.
// The generated form structure has changed, support both an additional
// 'data' key (that is not supported by tableselect) and the old version
// without.
if (isset($option['data'])) {
array_splice($option['data'], -1, 0, array($additional));
// Append the current option array to the form.
$form['languages']['#options'][$langcode] = $option['data'];
}
else {
array_splice($option, -1, 0, array($additional));
// Append the current option array to the form.
$form['languages']['#options'][$langcode] = $option;
}
}
$form['actions']['#type'] = 'actions';
$form['actions']['request'] = array(
'#type' => 'submit',
'#value' => t('Request translation'),
'#submit' => array('tmgmt_entity_ui_translate_form_submit'),
'#validate' => array('tmgmt_entity_ui_translate_form_validate'),
);
return $form;
}
/**
* Validation callback for the entity translation overview form.
*/
function tmgmt_entity_ui_translate_form_validate($form, &$form_state) {
$selected = array_filter($form_state['values']['languages']);
if (empty($selected)) {
form_set_error('languages', t('You have to select at least one language for requesting a translation.'));
}
}
/**
* Submit callback for the entity translation overview form.
*/
function tmgmt_entity_ui_translate_form_submit($form, &$form_state) {
$entity = $form_state['entity'];
$entity_type = $form_state['entity_type'];
list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
$values = $form_state['values'];
$jobs = array();
foreach (array_keys(array_filter($values['languages'])) as $langcode) {
// Create the job object.
$job = tmgmt_job_create(entity_language($entity_type, $entity), $langcode, $GLOBALS['user']->uid);
try {
// Add the job item.
$job->addItem('entity', $entity_type, $id);
// Append this job to the array of created jobs so we can redirect the user
// to a multistep checkout form if necessary.
$jobs[$job->tjid] = $job;
}
catch (TMGMTException $e) {
watchdog_exception('tmgmt', $e);
$languages = language_list();
$target_lang_name = $languages[$langcode]->language;
drupal_set_message(t('Unable to add job item for target language %name. Make sure the source content is not empty.', array('%name' => $target_lang_name)), 'error');
}
}
tmgmt_ui_job_checkout_and_redirect($form_state, $jobs);
}

View File

@@ -0,0 +1,328 @@
<?php
/**
* Basic Node Source tests.
*
*/
class TMGMTEntitySourceUITestCase extends TMGMTEntityTestCaseUtility {
static function getInfo() {
return array(
'name' => 'Entity Source UI tests',
'description' => 'Tests the user interface for entity translation sources.',
'group' => 'Translation Management',
'dependencies' => array('entity_translation'),
);
}
function setUp() {
parent::setUp(array('tmgmt_entity_ui', 'block', 'comment'));
variable_set('language_content_type_page', ENTITY_TRANSLATION_ENABLED);
variable_set('language_content_type_article', ENTITY_TRANSLATION_ENABLED);
$this->loginAsAdmin(array(
'create translation jobs',
'submit translation jobs',
'accept translation jobs',
'administer blocks',
'administer entity translation',
'toggle field translatability',
));
$this->setEnvironment('de');
$this->setEnvironment('fr');
$this->setEnvironment('es');
$this->setEnvironment('el');
$this->createNodeType('page', st('Page'), ENTITY_TRANSLATION_ENABLED);
$this->createNodeType('article', st('Article'), ENTITY_TRANSLATION_ENABLED);
// Enable path locale detection.
$edit = array(
'language[enabled][locale-url]' => TRUE,
'language_content[enabled][locale-interface]' => TRUE,
);
$this->drupalPost('admin/config/regional/language/configure', $edit, t('Save settings'));
// @todo Re-enable this when switching to testing profile.
// Enable the main page content block for hook_page_alter() to work.
$edit = array(
'blocks[system_main][region]' => 'content',
);
$this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
}
/**
* Test the translate tab for a single checkout.
*/
function testNodeTranslateTabSingleCheckout() {
$this->loginAsTranslator(array('translate node entities'));
// Create an english source node.
$node = $this->createNode('page', 'en');
// Create a nodes that will not be translated to test the missing
// translation filter.
$node_not_translated = $this->createNode('page', 'en');
$node_german = $this->createNode('page', 'de');
// Go to the translate tab.
$this->drupalGet('node/' . $node->nid);
$this->clickLink('Translate');
// Assert some basic strings on that page.
$this->assertText(t('Translations of @title', array('@title' => $node->title)));
$this->assertText(t('Pending Translations'));
// Request a translation for german.
$edit = array(
'languages[de]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
// Verify that we are on the translate tab.
$this->assertText(t('One job needs to be checked out.'));
$this->assertText($node->title);
// Submit.
$this->drupalPost(NULL, array(), t('Submit to translator'));
// Make sure that we're back on the translate tab.
$this->assertEqual(url('node/' . $node->nid . '/translate', array('absolute' => TRUE)), $this->getUrl());
$this->assertText(t('Test translation created.'));
$this->assertText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node->title, '@language' => t('German'))));
// Verify that the pending translation is shown.
$this->clickLink(t('Needs review'));
$this->drupalPost(NULL, array(), t('Save as completed'));
$this->assertText(t('The translation for @title has been accepted.', array('@title' => $node->title)));
// German node should now be listed and be clickable.
// @todo Improve detection of the link, e.g. use xpath on the table or the
// title module to get a better title.
$this->clickLink('view', 1);
$this->assertText('de_' . $node->body['en'][0]['value']);
// Test that the destination query argument does not break the redirect
// and we are redirected back to the correct page.
$this->drupalGet('node/' . $node->nid . '/translate', array('query' => array('destination' => 'node')));
// Request a spanish translation.
$edit = array(
'languages[es]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
// Verify that we are on the checkout page.
$this->assertText(t('One job needs to be checked out.'));
$this->assertText($node->title);
$this->drupalPost(NULL, array(), t('Submit to translator'));
// Make sure that we're back on the originally defined destination URL.
$this->assertEqual(url('node', array('absolute' => TRUE)), $this->getUrl());
// Test the missing translation filter.
$this->drupalGet('admin/tmgmt/sources');
$this->assertText($node->title);
$this->assertText($node_not_translated->title);
$this->drupalPost(NULL, array(
'search[target_language]' => 'de',
'search[target_status]' => 'untranslated',
), t('Search'));
$this->assertNoText($node->title);
$this->assertNoText($node_german->title);
$this->assertText($node_not_translated->title);
// Update the the translate flag of the translated node and test if it is
// listed among sources with missing translation.
db_update('entity_translation')->fields(array('translate' => 1))
->condition('entity_type', 'node')->condition('entity_id', $node->nid)->execute();
$this->drupalPost(NULL, array(
'search[target_language]' => 'de',
'search[target_status]' => 'outdated',
), t('Search'));
$this->assertText($node->title);
$this->assertNoText($node_german->title);
$this->assertNoText($node_not_translated->title);
$this->drupalPost(NULL, array(
'search[target_language]' => 'de',
'search[target_status]' => 'untranslated_or_outdated',
), t('Search'));
$this->assertText($node->title);
$this->assertNoText($node_german->title);
$this->assertText($node_not_translated->title);
}
/**
* Test the translate tab for a single checkout.
*/
function testNodeTranslateTabMultipeCheckout() {
// Allow auto-accept.
$default_translator = tmgmt_translator_load('test_translator');
$default_translator->settings = array(
'auto_accept' => TRUE,
);
$default_translator->save();
$this->loginAsTranslator(array('translate node entities'));
// Create an english source node.
$node = $this->createNode('page', 'en');
// Go to the translate tab.
$this->drupalGet('node/' . $node->nid);
$this->clickLink('Translate');
// Assert some basic strings on that page.
$this->assertText(t('Translations of @title', array('@title' => $node->title)));
$this->assertText(t('Pending Translations'));
// Request a translation for german.
$edit = array(
'languages[de]' => TRUE,
'languages[es]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
// Verify that we are on the translate tab.
$this->assertText(t('2 jobs need to be checked out.'));
// Submit all jobs.
$this->assertText($node->title);
$this->drupalPost(NULL, array(), t('Submit to translator and continue'));
$this->assertText($node->title);
$this->drupalPost(NULL, array(), t('Submit to translator'));
// Make sure that we're back on the translate tab.
$this->assertEqual(url('node/' . $node->nid . '/translate', array('absolute' => TRUE)), $this->getUrl());
$this->assertText(t('Test translation created.'));
$this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node->title, '@language' => t('Spanish'))));
$this->assertText(t('The translation for @title has been accepted.', array('@title' => $node->title)));
// Translated nodes should now be listed and be clickable.
// @todo Use links on translate tab.
$this->drupalGet('de/node/' . $node->nid);
$this->assertText('de_' . $node->body['en'][0]['value']);
$this->drupalGet('es/node/' . $node->nid);
$this->assertText('es_' . $node->body['en'][0]['value']);
}
/**
* Test translating comments.
*
* @todo: Disabled pending resolution of http://drupal.org/node/1760270.
*/
function dtestCommentTranslateTab() {
// Login as admin to be able to submit config page.
$this->loginAsAdmin(array('administer entity translation'));
// Enable comment translation.
$edit = array(
'entity_translation_entity_types[comment]' => TRUE
);
$this->drupalPost('admin/config/regional/entity_translation', $edit, t('Save configuration'));
// Change comment_body field to be translatable.
$comment_body = field_info_field('comment_body');
$comment_body['translatable'] = TRUE;
field_update_field($comment_body);
// Create a user that is allowed to translate comments.
$permissions = array('translate comment entities', 'create translation jobs', 'submit translation jobs', 'accept translation jobs', 'post comments', 'skip comment approval', 'edit own comments', 'access comments');
$entity_translation_permissions = entity_translation_permission();
// The new translation edit form of entity_translation requires a new
// permission that does not yet exist in older versions. Add it
// conditionally.
if (isset($entity_translation_permissions['edit original values'])) {
$permissions[] = 'edit original values';
}
$this->loginAsTranslator($permissions, TRUE);
// Create an english source term.
$node = $this->createNode('article', 'en');
// Add a comment.
$this->drupalGet('node/' . $node->nid);
$edit = array(
'subject' => $this->randomName(),
'comment_body[en][0][value]' => $this->randomName(),
);
$this->drupalPost(NULL, $edit, t('Save'));
$this->assertText(t('Your comment has been posted.'));
// Go to the translate tab.
$this->clickLink('edit');
$this->assertTrue(preg_match('|comment/(\d+)/edit$|', $this->getUrl(), $matches), 'Comment found');
$comment = comment_load($matches[1]);
$this->clickLink('Translate');
// Assert some basic strings on that page.
$this->assertText(t('Translations of @title', array('@title' => $comment->subject)));
$this->assertText(t('Pending Translations'));
// Request a translation for german.
$edit = array(
'languages[de]' => TRUE,
'languages[es]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
// Verify that we are on the translate tab.
$this->assertText(t('2 jobs need to be checked out.'));
// Submit all jobs.
$this->assertText($comment->subject);
$this->drupalPost(NULL, array(), t('Submit to translator and continue'));
$this->assertText($comment->subject);
$this->drupalPost(NULL, array(), t('Submit to translator'));
// Make sure that we're back on the translate tab.
$this->assertEqual(url('comment/' . $comment->cid . '/translate', array('absolute' => TRUE)), $this->getUrl());
$this->assertText(t('Test translation created.'));
$this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $comment->subject, '@language' => t('Spanish'))));
$this->assertText(t('The translation for @title has been accepted.', array('@title' => $comment->subject)));
// @todo Use links on translate tab.
$this->drupalGet('de/comment/' . $comment->cid);
$this->assertText('de_' . $comment->comment_body['en'][0]['value']);
// @todo Use links on translate tab.
$this->drupalGet('es/node/' . $comment->cid);
$this->assertText('es_' . $comment->comment_body['en'][0]['value']);
}
/**
* Test the entity source specific cart functionality.
*/
function testCart() {
$this->loginAsTranslator(array('translate node entities'));
$nodes = array();
for ($i = 0; $i < 4; $i++) {
$nodes[$i] = $this->createNode('page');
}
// Test the source overview.
$this->drupalGet('admin/tmgmt/sources/entity');
$this->drupalPost('admin/tmgmt/sources/entity', array(
'items[' . $nodes[1]->nid . ']' => TRUE,
'items[' . $nodes[2]->nid . ']' => TRUE,
), t('Add to cart'));
$this->drupalGet('admin/tmgmt/cart');
$this->assertText($nodes[1]->title);
$this->assertText($nodes[2]->title);
// Test the translate tab.
$this->drupalGet('node/' . $nodes[3]->nid . '/translate');
$this->assertRaw(t('There are @count items in the <a href="@url">translation cart</a>.',
array('@count' => 2, '@url' => url('admin/tmgmt/cart'))));
$this->drupalPost(NULL, array(), t('Add to cart'));
$this->assertRaw(t('@count content source was added into the <a href="@url">cart</a>.', array('@count' => 1, '@url' => url('admin/tmgmt/cart'))));
$this->assertRaw(t('There are @count items in the <a href="@url">translation cart</a> including the current item.',
array('@count' => 3, '@url' => url('admin/tmgmt/cart'))));
}
}

View File

@@ -0,0 +1,174 @@
<?php
/**
* Generic entity ui controller class for source plugin.
*
* @ingroup tmgmt_source
*/
class TMGMTEntitySourceUIController extends TMGMTEntityDefaultSourceUIController {
/**
* Gets overview form header.
*
* @return array
* Header array definition as expected by theme_tablesort().
*/
public function overviewFormHeader($type) {
$languages = array();
foreach (language_list() as $langcode => $language) {
$languages['langcode-' . $langcode] = array(
'data' => check_plain($language->name),
);
}
$entity_info = entity_get_info($type);
$header = array(
'title' => array('data' => t('Title (in source language)')),
);
// Show the bundle if there is more than one for this entity type.
if (count(tmgmt_entity_get_translatable_bundles($type)) > 1) {
$header['bundle'] = array('data' => t('@entity_name type', array('@entity_name' => $entity_info['label'])));
}
$header += $languages;
return $header;
}
/**
* Builds a table row for overview form.
*
* @param array $data
* Data needed to build the list row.
*
* @return array
*/
public function overviewRow($data) {
$label = $data['entity_label'] ? $data['entity_label'] : t('@type: @id', array('@type' => $data['entity_type'], '@id' => $data['entity_id']));
$row = array(
'id' => $data['entity_id'],
'title' => l($label, $data['entity_uri']),
);
if (isset($data['bundle'])) {
$row['bundle'] = $data['bundle'];
}
foreach (language_list() as $langcode => $language) {
$row['langcode-' . $langcode] = array(
'data' => theme('tmgmt_ui_translation_language_status_single', array(
'translation_status' => $data['translation_statuses'][$langcode],
'job_item' => isset($data['current_job_items'][$langcode]) ? $data['current_job_items'][$langcode] : NULL,
)),
'class' => array('langstatus-' . $langcode),
);
}
return $row;
}
/**
* {@inheritdoc}
*/
public function overviewForm($form, &$form_state, $type) {
$form += $this->overviewSearchFormPart($form, $form_state, $type);
$form['items'] = array(
'#type' => 'tableselect',
'#header' => $this->overviewFormHeader($type),
'#empty' => t('No entities matching given criteria have been found.'),
'#attributes' => array('id' => 'tmgmt-entities-list'),
);
// Load search property params which will be passed into
$search_property_params = array();
$exclude_params = array('q', 'page');
foreach ($_GET as $key => $value) {
// Skip exclude params, and those that have empty values, as these would
// make it into query condition instead of being ignored.
if (in_array($key, $exclude_params) || $value === '') {
continue;
}
$search_property_params[$key] = $value;
}
foreach ($this->getEntitiesTranslationData($type, $search_property_params) as $data) {
$form['items']['#options'][$data['entity_id']] = $this->overviewRow($data);
}
$form['pager'] = array('#markup' => theme('pager', array('tags' => NULL)));
return $form;
}
/**
* {@inheritdoc}
*/
public function overviewFormValidate($form, &$form_state, $type) {
if (!empty($form_state['values']['search']['target_language']) && $form_state['values']['search']['language'] == $form_state['values']['search']['target_language']) {
form_set_error('search[target_language]', t('The source and target languages must not be the same.'));
}
}
/**
* {@inheritdoc}
*/
public function overviewFormSubmit($form, &$form_state, $type) {
// Handle search redirect.
$this->overviewSearchFormRedirect($form, $form_state, $type);
$jobs = array();
$entities = entity_load($type, $form_state['values']['items']);
$source_lang_registry = array();
// Loop through entities and create individual jobs for each source language.
foreach ($entities as $entity) {
/**
* @var EntityTranslationDefaultHandler $handler
*/
$handler = entity_translation_get_handler($type, $entity);
$source_lang = entity_language($type, $entity);
list($entity_id, ,) = entity_extract_ids($type, $entity);
try {
// For given source lang no job exists yet.
if (!isset($source_lang_registry[$source_lang])) {
// Create new job.
$job = tmgmt_job_create($source_lang, NULL, $GLOBALS['user']->uid);
// Add initial job item.
$job->addItem('entity', $type, $entity_id);
// Add job identifier into registry
$source_lang_registry[$source_lang] = $job->tjid;
// Add newly created job into jobs queue.
$jobs[$job->tjid] = $job;
}
// We have a job for given source lang, so just add new job item for the
// existing job.
else {
$jobs[$source_lang_registry[$source_lang]]->addItem('entity', $type, $entity_id);
}
}
catch (TMGMTException $e) {
watchdog_exception('tmgmt', $e);
$entity_label = entity_label($type, $entity);
drupal_set_message(t('Unable to add job item for entity %name: %error.', array('%name' => $entity_label, '%error' => $e->getMessage())), 'error');
}
}
// If necessary, do a redirect.
$redirects = tmgmt_ui_job_checkout_multiple($jobs);
if ($redirects) {
tmgmt_ui_redirect_queue_set($redirects, current_path());
$form_state['redirect'] = tmgmt_ui_redirect_queue_dequeue();
drupal_set_message(format_plural(count($redirects), t('One job needs to be checked out.'), t('@count jobs need to be checked out.')));
}
}
}

View File

@@ -0,0 +1,106 @@
<?php
/*
* @file
* API documentation for this module.
*/
/**
* Alter source field data before being saved in a translation job.
*
* @param array $data
* Source field data.
* @param string $entity_type
* The entity type.
* @param object $entity
* An entity object.
* @param string $langcode
* The language of retrieved field values.
*/
function hook_tmgmt_field_source_data_alter(&$data, $entity_type, $entity, $langcode) {
}
/**
* Extract translatable text elements from a field.
*
* @param $entity_type
* The type of $entity.
* @param $entity
* The entity being extracted.
* @param $field
* The field structure.
* @param $instance
* The field instance.
* @param $langcode
* The language associated with $items.
* @param $items
* Array of values for this field.
*
* @return
* An array of translatable text elements, keyed by the schema name of the
* field.
*
* @see text_tmgmt_source_translation_structure()
*
* @ingroup tmgmt_source
*/
function hook_tmgmt_source_translation_structure($entity_type, $entity, $field, $instance, $langcode, $items) {
}
/**
* Puts data on the entity of the field type owned by the module.
*
* @param $entity_type
* The type of $entity.
* @param $entity
* The entity being extracted.
* @param $field
* The field structure.
* @param $instance
* The field instance.
* @param $langcode
* The language associated with $items.
* @param $data
* Translated data array.
* @param $use_field_translation
* TRUE if field translation is being used.
*
* @see tmgmt_field_populate_entity()
*/
function hook_tmgmt_field_type_populate_entity($entity_type, $entity, $field, $instance, $langcode, $data, $use_field_translation) {
}
/**
* Alter an entity object before populating its field with translations.
*
* @param array $data
* Source field data.
* @param object $entity
* An entity object.
* @param string $entity_type
* The entity type.
* @param string $langcode
* The language of retrieved field values.
*/
function hook_tmgmt_field_pre_populate_entity_alter(&$data, $entity, $entity_type, $langcode) {
}
/**
* Alter an entity object after populating its field with translations.
*
* @param object $entity
* An entity object.
* @param string $entity_type
* The entity type.
* @param array $data
* Source field data.
* @param string $langcode
* The language of retrieved field values.
*/
function hook_tmgmt_field_post_populate_entity_alter(&$entity, $entity_type, $data, $langcode) {
}

View File

@@ -0,0 +1,12 @@
name = Translation Management Field
description = Implements some hooks on behalf of the core Field modules that are required in the Translation Management module.
package = Translation Management
core = 7.x
test_dependencies[] = field_collection
; Information added by Drupal.org packaging script on 2016-09-21
version = "7.x-1.0-rc2+1-dev"
core = "7.x"
project = "tmgmt"
datestamp = "1474446494"

View File

@@ -0,0 +1,140 @@
<?php
/**
* @file
* Main module file for the Translation Management Source Field module.
*/
/**
* Implements hook_tmgmt_source_translation_structure().
*
* This hook is implemented on behalf of the core text module.
*/
function text_tmgmt_source_translation_structure($entity_type, $entity, $field, $instance, $langcode, $items) {
$structure = array();
if (!empty($items)) {
$structure['#label'] = check_plain($instance['label']);
foreach ($items as $delta => $value) {
$structure[$delta]['#label'] = t('Delta #@delta', array('@delta' => $delta));
$structure[$delta]['value'] = array(
'#label' => $structure['#label'],
'#text' => $value['value'],
'#translate' => TRUE,
);
// Add format.
$structure[$delta]['format'] = array(
'#label' => '',
'#text' => $value['format'],
'#translate' => FALSE,
);
if ($field['type'] == 'text_with_summary' && !empty($value['summary'])) {
$structure[$delta]['summary'] = array(
'#label' => t('Summary'),
'#text' => $value['summary'],
'#translate' => TRUE,
);
}
}
}
return $structure;
}
/**
* Helper function for retrieving all translatable field values from an entity.
*
* @param $entity_type
* The entity type.
* @param $entity
* An entity object.
* @param $langcode
* The language of retrieved field values.
* @param $only_translatable
* If TRUE, only the fields which are flagged as translatable are returned.
* Defaults to FALSE, which is usually used for node translation, where the
* field translatability does not matter.
*
* @return array
* The structured field data for all translatable fields
*/
function tmgmt_field_get_source_data($entity_type, $entity, $langcode, $only_translatable = FALSE) {
try {
list(, , $bundle) = entity_extract_ids($entity_type, $entity);
}
catch (Exception $e) {
watchdog_exception('tmgmt field', $e);
return array();
}
$fields = array();
foreach (field_info_instances($entity_type, $bundle) as $field_name => $instance) {
$field = field_info_field($field_name);
$items = field_get_items($entity_type, $entity, $field_name, $langcode);
if ((!$only_translatable || $field['translatable']) && $items) {
if ($data = module_invoke($field['module'], 'tmgmt_source_translation_structure', $entity_type, $entity, $field, $instance, $langcode, $items)) {
$fields[$field_name] = $data;
}
}
}
drupal_alter('tmgmt_field_source_data', $fields, $entity_type, $entity, $langcode);
return $fields;
}
/**
* Populates a field on an object with the provided field values.
*
* @param $entity_type
* The type of $entity.
* @param $entity
* The object to be populated.
* @param $langcode
* The field language.
* @param $data
* An array of values.
* @param $use_field_translation
* TRUE if field translation is being used.
*/
function tmgmt_field_populate_entity($entity_type, $entity, $langcode, $data, $use_field_translation = TRUE) {
drupal_alter('tmgmt_field_pre_populate_entity', $data, $entity, $entity_type, $langcode);
foreach (element_children($data) as $field_name) {
if ($field = field_info_field($field_name)) {
$function = $field['module'] . '_field_type_tmgmt_populate_entity';
list(, , $bundle) = entity_extract_ids($entity_type, $entity);
$instance = field_info_instance($entity_type, $field_name, $bundle);
if (function_exists($function)) {
$function($entity_type, $entity, $field, $instance, $langcode, $data, $use_field_translation);
}
else {
$field_langcode = $field['translatable'] ? $langcode : LANGUAGE_NONE;
// When not using field translation, make sure we're not storing
// multiple languages.
if (!$use_field_translation) {
$entity->{$field_name} = array($field_langcode => array());
}
foreach (element_children($data[$field_name]) as $delta) {
$columns = array();
foreach (element_children($data[$field_name][$delta]) as $column) {
if (isset($data[$field_name][$delta][$column]['#translation']['#text'])) {
$columns[$column] = $data[$field_name][$delta][$column]['#translation']['#text'];
}
// For elements which are not translatable, keep using the original
// value.
elseif (isset($data[$field_name][$delta][$column]['#translate']) && $data[$field_name][$delta][$column]['#translate'] == FALSE) {
$columns[$column] = $data[$field_name][$delta][$column]['#text'];
}
}
// Make sure the array_merge() gets an array as a first parameter.
if (!isset($entity->{$field_name}[$field_langcode][$delta])) {
$entity->{$field_name}[$field_langcode][$delta] = array();
}
$entity->{$field_name}[$field_langcode][$delta] = array_merge($entity->{$field_name}[$field_langcode][$delta], $columns);
}
}
}
}
drupal_alter('tmgmt_field_post_populate_entity', $entity, $entity_type, $data, $langcode);
}

View File

@@ -0,0 +1,20 @@
name = i18n String Source
description = i18n String source plugin for the Translation Management system.
package = Translation Management
core = 7.x
dependencies[] = tmgmt_ui
dependencies[] = i18n_string
# List variable as a dependency so that it gets picked up testbot.
# See http://drupal.org/node/1440484
dependencies[] = i18n
dependencies[] = variable
files[] = tmgmt_i18n_string.plugin.inc
files[] = tmgmt_i18n_string.test
files[] = tmgmt_i18n_string.ui.inc
; Information added by Drupal.org packaging script on 2016-09-21
version = "7.x-1.0-rc2+1-dev"
core = "7.x"
project = "tmgmt"
datestamp = "1474446494"

View File

@@ -0,0 +1,342 @@
<?php
/**
* @file
* Source plugin for the Translation Management system that handles i18n strings.
*/
/**
* Implements hook_tmgmt_source_plugin_info().
*/
function tmgmt_i18n_string_tmgmt_source_plugin_info() {
$info['i18n_string'] = array(
'label' => t('i18n String'),
'description' => t('Source handler for i18n strings.'),
'plugin controller class' => 'TMGMTI18nStringSourcePluginController',
'ui controller class' => 'TMGMTI18nStringDefaultSourceUIController',
);
foreach (i18n_object_info() as $object_type => $object_info) {
// Only consider object types that have string translation information.
if (isset($object_info['string translation'])) {
$info['i18n_string']['item types'][$object_type] = $object_info['title'];
}
}
return $info;
}
/**
* Gets i18n strings for given type and label.
*
* @param string $type
* i18n object type.
* @param string $search_label
* Label to search for.
* @param string $target_language
* Target language.
* @param string $target_status
* Target status.
*
* @return array
* List of i18n strings data.
*/
function tmgmt_i18n_string_get_strings($type, $search_label = NULL, $target_language = NULL, $target_status = 'untranslated_or_outdated') {
$info = i18n_object_info($type);
$languages = drupal_map_assoc(array_keys(language_list()));
$select = db_select('i18n_string', 'i18n_s');
$select->addTag('tmgmt_sources_search');
$select->addMetaData('plugin', 'i18n_string');
$select->addMetaData('type', $type);
$select->condition('i18n_s.textgroup', $info['string translation']['textgroup']);
if (!empty($target_language) && in_array($target_language, $languages)) {
if ($target_status == 'untranslated_or_outdated') {
$or = db_or();
$or->isNull("lt_$target_language.language");
$or->condition("lt_$target_language.i18n_status", I18N_STRING_STATUS_UPDATE);
$select->condition($or);
}
elseif ($target_status == 'outdated') {
$select->condition("lt_$target_language.i18n_status", I18N_STRING_STATUS_UPDATE);
}
elseif ($target_status == 'untranslated') {
$select->isNull("lt_$target_language.language");
}
}
if (isset($info['string translation']['type'])) {
$select->condition('i18n_s.type', $info['string translation']['type']);
}
elseif ($type == 'field' || $type == 'field_instance') {
// Fields and field instances share the same textgroup. Use list of bundles
// to include/exclude field_instances.
$bundles = array();
foreach (entity_get_info() as $entity_info) {
$bundles = array_merge($bundles, array_keys($entity_info['bundles']));
}
$select->condition('i18n_s.objectid', $bundles, $type == 'field_instance' ? 'IN' : 'NOT IN');
}
$select->join('locales_source', 'ls', 'ls.lid = i18n_s.lid');
$select->addField('ls', 'source');
if (!empty($search_label)) {
$select->condition('ls.source', "%$search_label%", 'LIKE');
}
foreach ($languages as $langcode) {
$langcode = str_replace('-', '', $langcode);
$select->leftJoin('locales_target', "lt_$langcode", "i18n_s.lid = %alias.lid AND %alias.language = '$langcode'");
$select->addField("lt_$langcode", 'language', "lang_$langcode");
}
$select->fields("i18n_s", array('lid', 'textgroup', 'context', 'type', 'objectid'));
$select->addExpression("concat(i18n_s.textgroup, ':', i18n_s.type, ':', i18n_s.objectid)", 'job_item_id');
$select->orderBy('i18n_s.context');
$select->groupBy('type');
$select->groupBy('objectid');
$select = $select->extend('PagerDefault')->limit(variable_get('tmgmt_source_list_limit', 20));
return $select->execute()->fetchAll();
}
/**
* Implements hook_form_ID_alter().
*
* Adds request translation capabilities into i18n translate tab.
*/
function tmgmt_i18n_string_form_i18n_string_translate_page_overview_form_alter(&$form, &$form_state) {
$object = $form['object']['#value'];
// Create the id: textgroup:type:objectid.
$id = $object->get_textgroup() . ':' . implode(':', $object->get_string_context());
$source_language = variable_get_value('i18n_string_source_language');
$existing_items = tmgmt_job_item_load_latest('i18n_string', $object->get_type(), $id, $source_language);
$form['top_actions']['#type'] = 'actions';
$form['top_actions']['#weight'] = -10;
tmgmt_ui_add_cart_form($form['top_actions'], $form_state, 'i18n_string', $object->get_type(), $id);
$form['languages']['#type'] = 'tableselect';
// Append lang code so that we can use it
foreach ($form['languages']['#rows'] as $lang => $row) {
if (isset($existing_items[$lang])) {
$states = tmgmt_job_item_states();
$row['status'] = $states[$existing_items[$lang]->state];
if ($existing_items[$lang]->isNeedsReview()) {
$row['operations'] .= ' | ' . l(t('review'), 'admin/tmgmt/items/' . $existing_items[$lang]->tjiid, array('query' => array('destination' => $_GET['q'])));
}
elseif ($existing_items[$lang]->isActive()) {
$row['operations'] .= ' | ' . l(t('in progress'), 'admin/tmgmt/items/' . $existing_items[$lang]->tjiid, array('query' => array('destination' => $_GET['q'])));
}
}
$form['languages']['#options'][$id . ':' . $lang] = $row;
if ($lang == $source_language || isset($existing_items[$lang])) {
$form['languages'][$id . ':' . $lang] = array(
'#type' => 'checkbox',
'#disabled' => TRUE,
);
}
}
unset($form['languages']['#rows'], $form['languages']['#theme']);
$form['actions']['request_translation'] = array(
'#type' => 'submit',
'#value' => t('Request translation'),
'#submit' => array('tmgmt_i18n_string_translate_form_submit'),
'#validate' => array('tmgmt_i18n_string_translate_form_validate'),
);
}
/**
* Validation callback for the entity translation overview form.
*/
function tmgmt_i18n_string_translate_form_validate($form, &$form_state) {
$selected = array_filter($form_state['values']['languages']);
if (empty($selected)) {
form_set_error('languages', t('You have to select at least one language for requesting a translation.'));
}
}
function tmgmt_i18n_string_translate_form_submit($form, &$form_state) {
$items = array_filter($form_state['values']['languages']);
$type = $form_state['values']['object']->get_type();
$source_lang = variable_get_value('i18n_string_source_language');
$jobs = array();
$target_lang_registry = array();
// Loop through entities and create individual jobs for each source language.
foreach ($items as $item) {
$item_parts = explode(':', $item);
$target_lang = array_pop($item_parts);
$key = implode(':', $item_parts);
// For given source lang no job exists yet.
if (!isset($target_lang_registry[$target_lang])) {
// Create new job.
$job = tmgmt_job_create($source_lang, $target_lang, $GLOBALS['user']->uid);
// Add initial job item.
$job->addItem('i18n_string', $type, $key);
// Add job identifier into registry
$target_lang_registry[$target_lang] = $job->tjid;
// Add newly created job into jobs queue.
$jobs[$job->tjid] = $job;
}
// We have a job for given source lang, so just add new job item for the
// existing job.
else {
$jobs[$target_lang_registry[$target_lang]]->addItem('i18n_string', $type, $key);
}
}
tmgmt_ui_job_checkout_and_redirect($form_state, $jobs);
}
/**
* Implements hook_i18n_object_info_alter().
*/
function tmgmt_i18n_string_i18n_object_info_alter(&$info) {
$entity_info = entity_get_info();
// Add a entity key to the object info if neither load callback nor entity
// keys are set and the object is an entity_type.
// @todo: Add this as default in EntityDefaultI18nStringController.
foreach ($info as $name => &$object) {
if (!isset($object['load callback']) && !isset($object['entity']) && isset($entity_info[$name])) {
$object['entity'] = $name;
}
}
}
/**
* Returns the i18n wrapper object.
*
* I18N objects with one or two keys are supported.
*
* @param string $type
* I18n object type.
* @param object $i18n_string
* Object with type and objectid properties.
*
* @return i18n_string_object_wrapper
*/
function tmgmt_i18n_string_get_wrapper($type, $i18n_string) {
$object_key = i18n_object_info($type, 'key');
// Special handling for i18nviews.
if ($type == 'views') {
// The construct method needs the full view object.
$view = views_get_view($i18n_string->objectid);
$wrapper = i18n_get_object($type, $i18n_string->objectid, $view);
return $wrapper;
}
// Special handling for i18n_panels.
$panels_objects = array(
'pane_configuration' => 'panels_pane',
'display_configuration' => 'panels_display',
);
if (in_array($type, array_keys($panels_objects))) {
ctools_include('export');
$wrapper = FALSE;
switch ($type) {
case 'display_configuration':
$object_array = ctools_export_load_object($panels_objects[$type], 'conditions', array('uuid' => $i18n_string->objectid));
$wrapper = i18n_get_object($type, $i18n_string->objectid, $object_array[$i18n_string->objectid]);
break;
case 'pane_configuration':
$obj = db_query("SELECT * FROM {panels_pane} WHERE uuid = :uuid", array(':uuid' => $i18n_string->objectid))->fetchObject();
if ($obj) {
$pane = ctools_export_unpack_object($panels_objects[$type], $obj);
$translation = i18n_panels_get_i18n_translation_object($pane);
$translation->uuid = $pane->uuid;
$wrapper = i18n_get_object($type, $i18n_string->objectid, $translation);
}
break;
default:
break;
}
return $wrapper;
}
// Special handling if the object has two keys. Assume that they
// mean type and object id.
if ($type == 'field') {
// Special case for fields which expect the type to be the identifier.
$wrapper = i18n_get_object($type, $i18n_string->type);
return $wrapper;
}
elseif ($type == 'field_instance') {
// Special case for field instances, which use the field name as type and
// bundle as object id. We don't know the entity_type, so we loop over all
// entity_types to search for the bundle. This will clash if different
// entity types have bundles with the same names.
foreach (entity_get_info() as $entity_type => $entity_info) {
if (isset($entity_info['bundles'][$i18n_string->objectid])) {
list($type_key, $objectid_key) = $object_key;
$wrapper = i18n_get_object($type, array(
$type_key => $i18n_string->type,
$objectid_key => $i18n_string->objectid
), field_info_instance($entity_type, $i18n_string->type, $i18n_string->objectid));
return $wrapper;
}
}
}
elseif (count($object_key) == 2) {
list($type_key, $objectid_key) = $object_key;
$wrapper = i18n_get_object($type, array(
$type_key => $i18n_string->type,
$objectid_key => $i18n_string->objectid
));
return $wrapper;
}
else {
// Otherwise, use the object id.
$wrapper = i18n_get_object($type, $i18n_string->objectid);
return $wrapper;
}
}
/**
* Implements hook_tmgmt_source_suggestions()
*/
function tmgmt_i18n_string_tmgmt_source_suggestions(array $items, TMGMTJob $job) {
$suggestions = array();
foreach ($items as $item) {
if (($item instanceof TMGMTJobItem) && ($item->item_type == 'node')) {
// Load translatable menu items related to this node.
$query = db_select('menu_links', 'ml')
->condition('ml.link_path', 'node/' . $item->item_id)
->fields('ml', array('mlid'));
$query->join('menu_custom', 'mc', 'ml.menu_name = mc.menu_name AND mc.i18n_mode = ' . I18N_MODE_MULTIPLE);
$results = $query->execute()->fetchAllAssoc('mlid');
foreach ($results as $result) {
$menu_link = menu_link_load($result->mlid);
// Add suggestion.
$suggestions[] = array(
'job_item' => tmgmt_job_item_create('i18n_string', 'menu_link', "menu:item:{$result->mlid}"),
'reason' => t('Menu link @title', array('@title' => $menu_link['link_title'])),
'from_item' => $item->tjiid,
);
}
}
}
return $suggestions;
}

View File

@@ -0,0 +1,157 @@
<?php
/**
* @file
* Provides the i18n string source controller.
*/
/**
* Translation plugin controller for i18n strings.
*/
class TMGMTI18nStringSourcePluginController extends TMGMTDefaultSourcePluginController {
/**
* {@inheritdoc}
*/
public function getData(TMGMTJobItem $job_item) {
$i18n_object = $this->getI18nObjectWrapper($job_item);
$structure = array();
$languages = language_list();
if ($i18n_object instanceof i18n_string_object_wrapper) {
$i18n_strings = $i18n_object->get_strings();
$source_language = $job_item->getJob()->source_language;
foreach ($i18n_strings as $string_id => $string) {
// If the job source language is different from the i18n source language
// try to load an existing translation for the language and use it as
// the source.
if ($source_language != i18n_string_source_language()) {
$translation = $string->get_translation($source_language);
if (empty($translation)) {
throw new TMGMTException(t('Unable to load %language translation for the string %title',
array('%language' => $languages[$source_language]->name, '%title' => $string->title)));
}
// If '#label' is empty theme_tmgmt_ui_translator_review_form() fails.
$structure[$string_id] = array(
'#label' => !empty($string->title) ? $string->title : $string->property,
'#text' => $translation,
'#translate' => TRUE
);
}
else {
// If '#label' is empty theme_tmgmt_ui_translator_review_form() fails.
$structure[$string_id] = array(
'#label' => !empty($string->title) ? $string->title : $string->property,
'#text' => $string->string,
'#translate' => TRUE
);
}
}
}
return $structure;
}
/**
* {@inheritdoc}
*/
public function saveTranslation(TMGMTJobItem $job_item) {
$job = tmgmt_job_load($job_item->tjid);
$data = array_filter(tmgmt_flatten_data($job_item->getData()), '_tmgmt_filter_data');
foreach ($data as $i18n_string => $item) {
if (isset($item['#translation']['#text'])) {
i18n_string_translation_update($i18n_string, $item['#translation']['#text'], $job->target_language);
}
}
// We just saved the translation, set the state of the job item to
// 'finished'.
$job_item->accepted();
}
/**
* {@inheritdoc}
*/
public function getLabel(TMGMTJobItem $job_item) {
if ($i18n_object = $this->getI18nObjectWrapper($job_item)) {
// Get the label, default to the get_title() method, fall back to the
// first string if that is empty.
$title = t('Unknown');
if ($i18n_object->get_title()) {
$title = $i18n_object->get_title();
}
elseif ($strings = $i18n_object->get_strings(array('empty' => TRUE))) {
$title = reset($strings)->get_string();
}
return t('@title (@id)', array('@title' => strip_tags(drupal_substr($title, 0, 64)), '@id' => $job_item->item_id));
}
return parent::getLabel($job_item);
}
/**
* {@inheritdoc}
*/
public function getUri(TMGMTJobItem $job_item) {
if ($wrapper = $this->getI18nObjectWrapper($job_item)) {
return array(
'path' => $wrapper->get_path(),
'options' => array(),
);
}
}
/**
* {@inheritdoc}
*/
public function getType(TMGMTJobItem $job_item) {
if ($label = $this->getItemTypeLabel($job_item->item_type)) {
return $label;
}
return parent::getType($job_item);
}
/**
* {@inheritdoc}
*/
public function getSourceLangCode(TMGMTJobItem $job_item) {
return i18n_string_source_language();
}
/**
* {@inheritdoc}
*/
public function getExistingLangCodes(TMGMTJobItem $job_item) {
$existing_lang_codes = array();
$languages = language_list();
if ($object = $this->getI18nObjectWrapper($job_item)) {
$existing_lang_codes = array_keys($languages);
foreach ($object->load_strings() as $string) {
foreach ($languages as $language) {
if ($language->language == $this->getSourceLangCode($job_item)) {
continue;
}
// Remove languages for which we fail to find translation.
if (in_array($language->language, $existing_lang_codes) && !$string->get_translation($language->language)) {
$existing_lang_codes = array_diff($existing_lang_codes, array($language->language));
}
}
}
}
return $existing_lang_codes;
}
/**
* Helper function to get i18n_object_wrapper for given job item.
*
* @param TMGMTJobItem $job_item
*
* @return i18n_string_object_wrapper
*/
protected function getI18nObjectWrapper(TMGMTJobItem $job_item) {
list(, $type, $object_id) = explode(':', $job_item->item_id, 3);
return tmgmt_i18n_string_get_wrapper($job_item->item_type, (object) array('type' => $type, 'objectid' => $object_id));
}
}

View File

@@ -0,0 +1,496 @@
<?php
/**
* Basic i18n String Source tests.
*/
class TMGMTI18nStringSourceTestCase extends TMGMTBaseTestCase {
static function getInfo() {
return array(
'name' => 'i18n String Source tests',
'description' => 'Exporting source data from i18n string and saving translations back',
'group' => 'Translation Management',
'dependencies' => array('i18n_string'),
);
}
function setUp() {
parent::setUp(array('tmgmt_ui', 'tmgmt_i18n_string', 'taxonomy', 'i18n_taxonomy', 'i18n_block', 'i18n_field', 'list', 'i18n_menu'));
$this->setEnvironment('de');
$this->translator = $this->createTranslator();
}
function testI18nStringSourceTaxonomy() {
// Test translation of a vocabulary.
/////////////////////////////////////
$config = array(
'name' => $this->randomName(),
'machine_name' => 'test_vocab',
'i18n_mode' => I18N_MODE_LOCALIZE,
);
$vocabulary = entity_create('taxonomy_vocabulary', $config);
taxonomy_vocabulary_save($vocabulary);
$string_object_name = "taxonomy:vocabulary:" . $vocabulary->vid;
$source_text = $vocabulary->name;
// Create the new job and job item.
$job = $this->createJob();
$job->translator = $this->translator->name;
$job->settings = array();
$job->save();
$item1 = $job->addItem('i18n_string', 'taxonomy_vocabulary', $string_object_name);
$this->assertEqual(t('Vocabulary'), $item1->getSourceType());
$job->requestTranslation();
foreach ($job->getItems() as $item) {
/* @var $item TMGMTJobItem */
$item->acceptTranslation();
}
// Check the structure of the imported data.
$this->assertEqual($item1->item_id, $string_object_name, 'i18n Strings object correctly saved');
// Check string translation.
$this->assertEqual(i18n_string_translate('taxonomy:vocabulary:' . $vocabulary->vid . ':name', $source_text, array('langcode' => $job->target_language)), $job->target_language . '_' . $source_text);
// Test translation of a taxonomy term.
/////////////////////////////////////
$term = entity_create('taxonomy_term', array(
'vid' => $vocabulary->vid,
'name' => $this->randomName(),
'description' => $this->randomName(),
));
taxonomy_term_save($term);
$string_object_name = "taxonomy:term:" . $term->tid;
$source_text_name = $term->name;
$source_text_description = $term->description;
// Create the new job and job item.
$job = $this->createJob();
$job->translator = $this->translator->name;
$job->settings = array();
$job->save();
$item1 = $job->addItem('i18n_string', 'taxonomy_term', $string_object_name);
$this->assertEqual(t('Taxonomy term'), $item1->getSourceType());
$job->requestTranslation();
/* @var $item TMGMTJobItem */
foreach ($job->getItems() as $item) {
// The source is available only in en.
$this->assertJobItemLangCodes($item, 'en', array('en'));
$item->acceptTranslation();
// The source should be now available in de and en.
$this->assertJobItemLangCodes($item, 'en', array('de', 'en'));
}
// Check the structure of the imported data.
$this->assertEqual($item1->item_id, $string_object_name);
// Check string translation.
$this->assertEqual(i18n_string_translate('taxonomy:term:' . $term->tid . ':name', $source_text_name,
array('langcode' => $job->target_language)), $job->target_language . '_' . $source_text_name);
$this->assertEqual(i18n_string_translate('taxonomy:term:' . $term->tid . ':description', $source_text_description,
array('langcode' => $job->target_language)), $job->target_language . '_' . $source_text_description);
}
/**
* Test if the source is able to pull content in requested language.
*/
function testRequestDataForSpecificLanguage() {
$this->setEnvironment('es');
$this->setEnvironment('cs');
$config = array(
'name' => $this->randomName(),
'machine_name' => 'test_vocab',
'i18n_mode' => I18N_MODE_LOCALIZE,
);
$vocabulary = entity_create('taxonomy_vocabulary', $config);
taxonomy_vocabulary_save($vocabulary);
$string_object_name = "taxonomy:vocabulary:" . $vocabulary->vid;
i18n_string_translation_update($string_object_name . ':name', 'de translation', 'de');
// Create new job item with a source language for which the translation
// exits.
$job = $this->createJob('de', 'cs');
$job->save();
$job->addItem('i18n_string', 'taxonomy_vocabulary', $string_object_name);
$data = $job->getData();
$this->assertEqual($data[1][$string_object_name . ':name']['#text'], 'de translation');
// Create new job item with a source language for which the translation
// does not exit.
$job = $this->createJob('es', 'cs');
$job->save();
try {
$job->addItem('i18n_string', 'taxonomy_vocabulary', $string_object_name);
$this->fail('The job item should not be added as there is no translation for language "es"');
}
catch (TMGMTException $e) {
$languages = language_list();
$this->assertEqual(t('Unable to load %language translation for the string %title',
array('%language' => $languages['es']->name, '%title' => 'Name')), $e->getMessage());
}
}
function testI18nStringSourceMenu() {
drupal_static_reset('_tmgmt_plugin_info');
drupal_static_reset('_tmgmt_plugin_controller');
// Test translation of a menu.
/////////////////////////////////////
$config = array(
'menu_name' => $this->randomName(),
'title' => $this->randomName(),
'description' => $this->randomName(),
'i18n_mode' => I18N_MODE_MULTIPLE,
);
menu_save($config);
$menu = menu_load($config['menu_name']);
$source_text = $menu['title'];
$string_name = 'menu:menu:' . $menu['menu_name'];
// Create the new job and job item.
$job = $this->createJob();
$job->translator = $this->translator->name;
$job->settings = array();
$item1 = $job->addItem('i18n_string', 'menu', $string_name);
$this->assertEqual(t('Menu'), $item1->getSourceType());
$job->requestTranslation();
/* @var $item TMGMTJobItem */
foreach ($job->getItems() as $item) {
$this->assertJobItemLangCodes($item, 'en', array('en'));
$item->acceptTranslation();
$this->assertJobItemLangCodes($item, 'en', array('de', 'en'));
}
$data = $item1->getData();
$this->assertEqual($data['menu:menu:' . $menu['menu_name'] . ':title']['#text'], $config['title']);
$this->assertEqual($data['menu:menu:' . $menu['menu_name'] . ':description']['#text'], $config['description']);
// Check the structure of the imported data.
$this->assertEqual($item1->item_id, $string_name, 'String is correctly saved');
// Check string translation.
$this->assertEqual(i18n_string_translate($string_name . ':title', $source_text, array('langcode' => $job->target_language)), $job->target_language . '_' . $source_text);
// Test translation of a menu item.
/////////////////////////////////////
$source_text = $this->randomName();
$menu_link = array(
'link_path' => '<front>',
'link_title' => $source_text,
// i18n_menu_link::get_title() uses the title, set that too.
'title' => $source_text,
'menu_name' => $menu['menu_name'],
'customized' => TRUE,
);
menu_link_save($menu_link);
$string_name = 'menu:item:' . $menu_link['mlid'];
// Create the new job and job item.
$job = $this->createJob();
$job->translator = $this->translator->name;
$job->settings = array();
$item1 = $job->addItem('i18n_string', 'menu_link', $string_name);
$this->assertEqual(t('Menu link'), $item1->getSourceType());
$job->requestTranslation();
/* @var $item TMGMTJobItem */
foreach ($job->getItems() as $item) {
$this->assertJobItemLangCodes($item, 'en', array('en'));
$item->acceptTranslation();
$this->assertJobItemLangCodes($item, 'en', array('de', 'en'));
}
$data = $item1->getData();
$this->assertEqual($data[$string_name . ':title']['#text'], $source_text);
// Check the structure of the imported data.
$this->assertEqual($item1->item_id, $string_name);
// Check string translation.
$this->assertEqual(i18n_string_translate($string_name . ':title', $source_text, array('langcode' => $job->target_language)), $job->target_language . '_' . $source_text);
}
function testI18nStringSourceLangCodes() {
$config = array(
'name' => $this->randomName(),
'description' => 'description_' . $this->randomName(),
'machine_name' => 'test_vocab',
'i18n_mode' => I18N_MODE_LOCALIZE,
);
$vocabulary = entity_create('taxonomy_vocabulary', $config);
taxonomy_vocabulary_save($vocabulary);
$string_object_name = "taxonomy:vocabulary:" . $vocabulary->vid;
// Create the new job and job item.
$job = $this->createJob();
$job->translator = $this->translator->name;
$job->settings = array();
$job->save();
$item = $job->addItem('i18n_string', 'taxonomy_vocabulary', $string_object_name);
$this->assertJobItemLangCodes($item, 'en', array('en'));
i18n_string_translation_update($string_object_name . ':description', 'de_' . $config['description'], 'de');
$this->assertJobItemLangCodes($item, 'en', array('en'));
i18n_string_translation_update($string_object_name . ':name', 'de_' . $config['name'], 'de');
$this->assertJobItemLangCodes($item, 'en', array('en', 'de'));
}
function testI18nStringPluginUI() {
$this->loginAsAdmin(array('administer taxonomy', 'translate interface', 'translate user-defined strings'));
$vocab_data = array(
'name' => $this->randomName(),
'machine_name' => 'test_vocab',
'i18n_mode' => I18N_MODE_LOCALIZE,
);
$term_data = array(
'name' => $this->randomName(),
);
$vocab_data_not_translated = array(
'name' => $this->randomName(),
'machine_name' => 'test_vocab3',
'i18n_mode' => I18N_MODE_LOCALIZE,
);
// Configure taxonomy and create vocab + term.
$this->drupalPost('admin/structure/taxonomy/add', $vocab_data, t('Save'));
$this->drupalGet('admin/structure/taxonomy');
$this->clickLink(t('add terms'));
$this->drupalPost(NULL, $term_data, t('Save'));
$this->drupalPost('admin/structure/taxonomy/add', $vocab_data_not_translated, t('Save'));
$this->drupalGet('admin/tmgmt/sources/i18n_string_taxonomy_vocabulary');
$this->assertText($vocab_data['name']);
// Request translation via i18n source tab
$this->drupalPost(NULL, array('items[taxonomy:vocabulary:1]' => 1), t('Request translation'));
// Test for the job checkout url.
$this->assertTrue(strpos($this->getUrl(), 'admin/tmgmt/jobs') !== FALSE);
entity_get_controller('tmgmt_job')->resetCache();
$jobs = entity_load('tmgmt_job', FALSE);
/** @var TMGMTJob $job */
$job = array_pop($jobs);
$this->assertFieldByName('label', $job->label());
// Request translation via translate tab of i18n.
$this->drupalPost('admin/structure/taxonomy/test_vocab/translate', array('languages[taxonomy:vocabulary:1:de]' => 1), t('Request translation'));
$this->drupalPost(NULL, array(), t('Submit to translator'));
// Verify that the job item status is shown.
$this->assertText(t('Needs review'));
$this->clickLink(t('review'));
$this->drupalPost(NULL, array(), t('Save as completed'));
$this->assertText(t('The translation for @label has been accepted.', array('@label' => $job->label())));
// Test the missing translation filter.
$this->drupalGet('admin/tmgmt/sources/i18n_string_taxonomy_vocabulary');
// Check that the source language has been removed from the target language
// select box.
$elements = $this->xpath('//select[@name=:name]//option[@value=:option]', array(':name' => 'search[target_language]', ':option' => i18n_string_source_language()));
$this->assertTrue(empty($elements));
$edit = array(
'search[target_language]' => 'de',
'search[target_status]' => 'untranslated',
);
$this->drupalPost('admin/tmgmt/sources/i18n_string_taxonomy_vocabulary', $edit, t('Search'));
// The vocabulary name is translated to "de" therefore it must not show up
// in the list.
$this->assertNoText($vocab_data['name']);
$this->assertText($vocab_data_not_translated['name']);
$edit = array(
'search[target_language]' => 'de',
'search[target_status]' => 'untranslated',
);
$this->drupalPost(NULL, $edit, t('Search'));
$this->assertNoText($vocab_data['name']);
$this->assertText($vocab_data_not_translated['name']);
// Update the string status to I18N_STRING_STATUS_UPDATE.
$lid = db_select('locales_source', 's')->fields('s', array('lid'))->condition('source', $vocab_data['name'])->execute()->fetchField();
db_update('locales_target')->fields(array('i18n_status' => I18N_STRING_STATUS_UPDATE))->condition('lid', $lid)->execute();
$edit = array(
'search[target_language]' => 'de',
'search[target_status]' => 'outdated',
);
$this->drupalPost(NULL, $edit, t('Search'));
$this->assertText($vocab_data['name']);
$this->assertNoText($vocab_data_not_translated['name']);
$edit = array(
'search[target_language]' => 'de',
'search[target_status]' => 'untranslated_or_outdated',
);
$this->drupalPost(NULL, $edit, t('Search'));
$this->assertText($vocab_data['name']);
$this->assertText($vocab_data_not_translated['name']);
}
/**
* Tests translation of blocks through the user interface.
*/
function testI18nStringPluginUIBlock() {
$this->loginAsAdmin(array('administer blocks', 'translate interface', 'translate user-defined strings'));
// Make some blocks translatable.
$navigation_edit = array(
'title' => $this->randomName(),
'i18n_mode' => 1,
);
$this->drupalPost('admin/structure/block/manage/system/navigation/configure', $navigation_edit, t('Save block'));
$powered_edit = array(
'title' => $this->randomName(),
'i18n_mode' => 1,
);
$this->drupalPost('admin/structure/block/manage/system/powered-by/configure', $powered_edit, t('Save block'));
$this->drupalGet('admin/tmgmt/sources/i18n_string_block');
$this->assertText($navigation_edit['title']);
$this->assertText($powered_edit['title']);
// Request translation via i18n source tab.
$edit = array(
'items[blocks:system:powered-by]' => 1,
'items[blocks:system:navigation]' => 1,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
$this->assertText($navigation_edit['title']);
$this->assertText($powered_edit['title']);
$this->drupalPost(NULL, array(), t('Submit to translator'));
$this->assertRaw(t('Active job item: Needs review'));
}
/**
* Tests translation of fields through the user interface.
*/
function testI18nStringPluginUIField() {
$this->loginAsAdmin(array('translate interface', 'translate user-defined strings'));
$type = $this->drupalCreateContentType(array('type' => $type = $this->randomName()));
// Create a field.
$field = array(
'field_name' => 'list_test',
'type' => 'list_text',
);
for ($i = 0; $i < 5; $i++) {
$field['settings']['allowed_values'][$this->randomName()] = $this->randomString();
}
field_create_field($field);
// Create an instance of the previously created field.
$instance = array(
'field_name' => 'list_test',
'entity_type' => 'node',
'bundle' => $type->type,
'label' => $this->randomName(10),
'description' => $this->randomString(30),
);
field_create_instance($instance);
// The body field doesn't have anything that can be translated on the field
// level, so it shouldn't show up in the field overview.
$this->drupalGet('admin/tmgmt/sources/i18n_string_field');
$this->assertNoText(t('Body'));
// @todo: Label doesn't work here?
$this->assertText('field:list_test:#allowed_values');
$this->drupalGet('admin/tmgmt/sources/i18n_string_field_instance');
$this->assertUniqueText(t('Body'));
$this->assertUniqueText($instance['label']);
// Request translation.
$edit = array(
'items[field:body:' . $type->type . ']' => 1,
'items[field:list_test:' . $type->type . ']' => 1,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
$this->assertText(t('Body'));
$this->assertText($instance['label']);
$this->drupalPost(NULL, array(), t('Submit to translator'));
$this->assertRaw(t('Active job item: Needs review'));
// Review the first item.
$this->clickLink(t('reviewed'));
$this->drupalPost(NULL, array(), t('Save as completed'));
// The overview should now have a translated field and a pending job item.
$this->drupalGet('admin/tmgmt/sources/i18n_string_field_instance');
$this->assertRaw(t('Translation up to date'));
$this->assertRaw(t('Active job item: Needs review'));
}
/**
* Test the i18n specific cart functionality.
*/
function testCart() {
$vocabulary1 = entity_create('taxonomy_vocabulary', array(
'name' => $this->randomName(),
'description' => 'description_' . $this->randomName(),
'machine_name' => 'test_vocab1',
'i18n_mode' => I18N_MODE_LOCALIZE,
));
taxonomy_vocabulary_save($vocabulary1);
$string1 = "taxonomy:vocabulary:" . $vocabulary1->vid;
$vocabulary2 = entity_create('taxonomy_vocabulary', array(
'name' => $this->randomName(),
'description' => 'description_' . $this->randomName(),
'machine_name' => 'test_vocab2',
'i18n_mode' => I18N_MODE_LOCALIZE,
));
taxonomy_vocabulary_save($vocabulary2);
$string2 = "taxonomy:vocabulary:" . $vocabulary2->vid;
$vocabulary3 = entity_create('taxonomy_vocabulary', array(
'name' => $this->randomName(),
'description' => 'description_' . $this->randomName(),
'machine_name' => 'test_vocab3',
'i18n_mode' => I18N_MODE_LOCALIZE,
));
taxonomy_vocabulary_save($vocabulary3);
$this->loginAsAdmin(array_merge($this->translator_permissions, array('translate interface', 'translate user-defined strings')));
// Test source overview.
$this->drupalPost('admin/tmgmt/sources/i18n_string_taxonomy_vocabulary', array(
'items[' . $string1 . ']' => TRUE,
'items[' . $string2 . ']' => TRUE,
), t('Add to cart'));
$this->drupalGet('admin/tmgmt/cart');
$this->assertText($vocabulary1->name);
$this->assertText($vocabulary2->name);
// Test translate tab.
$this->drupalGet('admin/structure/taxonomy/test_vocab3/translate');
$this->assertRaw(t('There are @count items in the <a href="@url">translation cart</a>.',
array('@count' => 2, '@url' => url('admin/tmgmt/cart'))));
$this->drupalPost(NULL, array(), t('Add to cart'));
$this->assertRaw(t('@count content source was added into the <a href="@url">cart</a>.', array('@count' => 1, '@url' => url('admin/tmgmt/cart'))));
$this->assertRaw(t('There are @count items in the <a href="@url">translation cart</a> including the current item.',
array('@count' => 3, '@url' => url('admin/tmgmt/cart'))));
}
}

View File

@@ -0,0 +1,295 @@
<?php
/**
* @file
* Provides the I18nString source controller.
*/
/**
* Class TMGMTI18nStringDefaultSourceUIController
*
* UI Controller fo i18n strings translation jobs.
*/
class TMGMTI18nStringDefaultSourceUIController extends TMGMTDefaultSourceUIController {
/**
* Gets overview form header.
*
* @return array
* Header array definition as expected by theme_tablesort().
*/
public function overviewFormHeader() {
$languages = array();
foreach (language_list() as $langcode => $language) {
$langcode = str_replace('-', '', $langcode);
$languages['langcode-' . $langcode] = array(
'data' => check_plain($language->name),
);
}
$header = array(
'title' => array('data' => t('Label (in source language)')),
'type' => array('data' => t('Type')),
) + $languages;
return $header;
}
/**
* {@inheritdoc}
*/
public function overviewForm($form, &$form_state, $type) {
$form += $this->overviewSearchFormPart($form, $form_state, $type);
$form['items'] = array(
'#type' => 'tableselect',
'#header' => $this->overviewFormHeader($type),
'#empty' => t('No strings matching given criteria have been found.')
);
$search_data = $this->getSearchFormSubmittedParams();
$i18n_strings = tmgmt_i18n_string_get_strings($type, $search_data['label'], $search_data['target_language'], $search_data['target_status']);
foreach ($this->getTranslationData($i18n_strings, $form_state['item_type']) as $id => $data) {
$form['items']['#options'][$id] = $this->overviewRow($type, $data);
}
$form['pager'] = array('#markup' => theme('pager', array('tags' => NULL)));
return $form;
}
/**
* Helper function to create translation data list for the sources page list.
*
* @param array $i18n_strings
* Result of the search query returned by tmgmt_i18n_string_get_strings().
* @param string $type
* I18n object type.
*
* @return array
* Structured array with translation data.
*/
protected function getTranslationData($i18n_strings, $type) {
$objects = array();
$source_language = variable_get_value('i18n_string_source_language');
foreach ($i18n_strings as $i18n_string) {
$wrapper = tmgmt_i18n_string_get_wrapper($type, $i18n_string);
if ($wrapper instanceof i18n_string_object_wrapper) {
$id = $i18n_string->job_item_id;
// Get existing translations and current job items for the entity
// to determine translation statuses
$current_job_items = tmgmt_job_item_load_latest('i18n_string', $wrapper->get_type(), $id, $source_language);
$objects[$id] = array(
'id' => $id,
'object' => $wrapper->get_strings(array('empty' => TRUE)),
'wrapper' => $wrapper,
);
// Load entity translation specific data.
foreach (language_list() as $langcode => $language) {
$langcode = str_replace('-', '', $langcode);
$translation_status = 'current';
if ($langcode == $source_language) {
$translation_status = 'original';
}
elseif ($i18n_string->{'lang_' . $langcode} === NULL) {
$translation_status = 'missing';
}
$objects[$id]['current_job_items'][$langcode] = isset($current_job_items[$langcode]) ? $current_job_items[$langcode] : NULL;
$objects[$id]['translation_statuses'][$langcode] = $translation_status;
}
}
}
return $objects;
}
/**
* Builds search form for entity sources overview.
*
* @param array $form
* Drupal form array.
* @param $form_state
* Drupal form_state array.
* @param $type
* Entity type.
*
* @return array
* Drupal form array.
*/
public function overviewSearchFormPart($form, &$form_state, $type) {
$options = array();
foreach (language_list() as $langcode => $language) {
$options[$langcode] = $language->name;
}
$default_values = $this->getSearchFormSubmittedParams();
$form['search_wrapper'] = array(
'#prefix' => '<div class="tmgmt-sources-wrapper tmgmt-i18n_string-sources-wrapper">',
'#suffix' => '</div>',
'#weight' => -15,
);
$form['search_wrapper']['search'] = array(
'#tree' => TRUE,
);
$form['search_wrapper']['search']['label'] = array(
'#type' => 'textfield',
'#title' => t('Label in source language'),
'#default_value' => isset($default_values['label']) ? $default_values['label'] : NULL,
);
// Unset the source language as it should not be listed among target
// languages.
unset($options[i18n_string_source_language()]);
$form['search_wrapper']['search']['target_language'] = array(
'#type' => 'select',
'#title' => t('Target language'),
'#options' => $options,
'#empty_option' => t('Any'),
'#default_value' => isset($default_values['target_language']) ? $default_values['target_language'] : NULL,
);
$form['search_wrapper']['search']['target_status'] = array(
'#type' => 'select',
'#title' => t('Target status'),
'#options' => array(
'untranslated_or_outdated' => t('Untranslated or outdated'),
'untranslated' => t('Untranslated'),
'outdated' => t('Outdated'),
),
'#default_value' => isset($default_values['target_status']) ? $default_values['target_status'] : NULL,
'#states' => array(
'invisible' => array(
':input[name="search[target_language]"]' => array('value' => ''),
),
),
);
$form['search_wrapper']['search_submit'] = array(
'#type' => 'submit',
'#value' => t('Search'),
);
return $form;
}
/**
* Gets submitted search params.
*
* @return array
*/
public function getSearchFormSubmittedParams() {
$params = array(
'label' => NULL,
'target_language' => NULL,
'target_status' => NULL,
);
if (isset($_GET['label'])) {
$params['label'] = $_GET['label'];
}
if (isset($_GET['target_language'])) {
$params['target_language'] = $_GET['target_language'];
}
if (isset($_GET['target_status'])) {
$params['target_status'] = $_GET['target_status'];
}
return $params;
}
/**
* Builds a table row for overview form.
*
* @param string $type
* i18n type.
* @param array $data
* Data needed to build the list row.
*
* @return array
*/
public function overviewRow($type, $data) {
// Set the default item key, assume it's the first.
$item_title = reset($data['object']);
$type_label = i18n_object_info($type, 'title');
$row = array(
'id' => $data['id'],
'title' => $item_title->get_string() ? t('@title (@id)', array('@title' => $item_title->get_string(), '@id' => $data['id'])) : $data['id'],
'type' => empty($type_label) ? t('Unknown') : $type_label,
);
foreach (language_list() as $langcode => $language) {
$langcode = str_replace('-', '', $langcode);
$row['langcode-' . $langcode] = theme('tmgmt_ui_translation_language_status_single', array(
'translation_status' => $data['translation_statuses'][$langcode],
'job_item' => isset($data['current_job_items'][$langcode]) ? $data['current_job_items'][$langcode] : NULL,
));
}
return $row;
}
/**
* {@inheritdoc}
*/
public function overviewFormSubmit($form, &$form_state, $type) {
// Handle search redirect.
$this->overviewSearchFormRedirect($form, $form_state, $type);
$items = array_filter($form_state['values']['items']);
$type = $form_state['item_type'];
$source_lang = variable_get_value('i18n_string_source_language');
// Create only single job for all items as the source language is just
// the same for all.
$job = tmgmt_job_create($source_lang, NULL, $GLOBALS['user']->uid);
// Loop through entities and create individual jobs for each source language.
foreach ($items as $item) {
$job->addItem('i18n_string', $type, $item);
}
$form_state['redirect'] = array('admin/tmgmt/jobs/' . $job->tjid,
array('query' => array('destination' => current_path())));
drupal_set_message(t('One job needs to be checked out.'));
}
/**
* Performs redirect with search params appended to the uri.
*
* In case of triggering element is edit-search-submit it redirects to
* current location with added query string containing submitted search form
* values.
*
* @param array $form
* Drupal form array.
* @param $form_state
* Drupal form_state array.
* @param $type
* Entity type.
*/
public function overviewSearchFormRedirect($form, &$form_state, $type) {
if ($form_state['triggering_element']['#id'] == 'edit-search-submit') {
$query = array();
foreach ($form_state['values']['search'] as $key => $value) {
$query[$key] = $value;
}
drupal_goto($_GET['q'], array('query' => $query));
}
}
}

View File

@@ -0,0 +1,13 @@
msgid ""
msgstr ""
"Project-Id-Version: Drupal 7\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\\n"
msgid "Hello World"
msgstr "Hallo Welt"
msgid "Example"
msgstr "Beispiel"

View File

@@ -0,0 +1,17 @@
name = Locales Source
description = Locales source plugin for the Translation Management system.
package = Translation Management
core = 7.x
dependencies[] = tmgmt
dependencies[] = locale
files[] = tmgmt_locale.plugin.inc
files[] = tmgmt_locale.test
files[] = tmgmt_locale.ui.inc
files[] = tmgmt_locale.ui.test
; Information added by Drupal.org packaging script on 2016-09-21
version = "7.x-1.0-rc2+1-dev"
core = "7.x"
project = "tmgmt"
datestamp = "1474446494"

View File

@@ -0,0 +1,28 @@
<?php
/**
* @file
* Installation hooks for tmgmt_locale module.
*/
/**
* Update existing {locales_target}.l10n_status if any.
*/
function tmgmt_locale_update_7000() {
if (module_exists('l10n_update')) {
module_load_include('inc', 'l10n_update');
$query = db_select('tmgmt_job_item', 'ji')
->condition('ji.plugin', 'locale')
->condition('ji.state', TMGMT_JOB_ITEM_STATE_ACCEPTED);
$query->innerJoin('tmgmt_job', 'j', 'j.tjid = ji.tjid');
$query->addField('ji', 'item_id', 'lid');
$query->addField('j', 'target_language', 'language');
foreach ($query->execute() as $row) {
db_update('locales_target')
->condition('lid', $row->lid)
->condition('language', $row->language)
->fields(array('l10n_status' => L10N_UPDATE_STRING_CUSTOM))
->execute();
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* @file
* Source plugin for the Translation Management system that handles locale strings.
*/
/**
* Implements hook_tmgmt_source_plugin_info().
*
* @see TMGMTLocaleSourcePluginController
*/
function tmgmt_locale_tmgmt_source_plugin_info() {
$info['locale'] = array(
'label' => t('Locale source'),
'description' => t('Source handler for locale strings.'),
'plugin controller class' => 'TMGMTLocaleSourcePluginController',
'ui controller class' => 'TMGMTLocaleSourceUIController',
'item types' => array(
'default' => t('Locale'),
),
);
return $info;
}

View File

@@ -0,0 +1,245 @@
<?php
/**
* @file
* Provides the locale source controller.
*/
/**
* Translation plugin controller for locale strings.
*/
class TMGMTLocaleSourcePluginController extends TMGMTDefaultSourcePluginController {
/**
* Updates translation associated to a specific locale source.
*
* @param string $lid
* The Locale ID.
* @param string $target_language
* Target language to update translation.
* @param string $translation
* Translation value.
*
* @return bool
* Success or not updating the locale translation.
*/
protected function updateTranslation($lid, $target_language, $translation) {
$languages = locale_language_list('name', TRUE);
if (!$lid || !array_key_exists($target_language, $languages) || !$translation) {
return FALSE;
}
$exists = db_query("SELECT COUNT(lid) FROM {locales_target} WHERE lid = :lid AND language = :language", array(':lid' => $lid, ':language' => $target_language))
->fetchField();
$fields = array(
'translation' => $translation,
);
if (module_exists('l10n_update')) {
module_load_include('inc', 'l10n_update');
$fields += array(
'l10n_status' => L10N_UPDATE_STRING_CUSTOM,
);
}
// @todo Only singular strings are managed here, we should take care of
// plural information of processed string.
if (!$exists) {
$fields += array(
'lid' => $lid,
'language' => $target_language,
);
db_insert('locales_target')
->fields($fields)
->execute();
}
else {
db_update('locales_target')
->fields($fields)
->condition('lid', $lid)
->condition('language', $target_language)
->execute();
}
// Clear locale caches.
_locale_invalidate_js($target_language);
cache_clear_all('locale:' . $target_language, 'cache');
return TRUE;
}
/**
* Helper function to obtain a locale object for given job item.
*
* @param TMGMTJobItem $job_item
*
* @return locale object
*/
protected function getLocaleObject(TMGMTJobItem $job_item) {
$locale_lid = $job_item->item_id;
// Check existence of assigned lid.
$exists = db_query("SELECT COUNT(lid) FROM {locales_source} WHERE lid = :lid", array(':lid' => $locale_lid))->fetchField();
if (!$exists) {
throw new TMGMTException(t('Unable to load locale with id %id', array('%id' => $job_item->item_id)));
}
// This is necessary as the method is also used in the getLabel() callback
// and for that case the job is not available in the cart.
if (!empty($job_item->tjid)) {
$source_language = $job_item->getJob()->source_language;
}
else {
$source_language = $job_item->getSourceLangCode();
}
if ($source_language == 'en') {
$query = db_select('locales_source', 'ls');
$query
->fields('ls')
->condition('ls.lid', $locale_lid);
$locale_object = $query
->execute()
->fetchObject();
$locale_object->language = 'en';
if (empty($locale_object)) {
return null;
}
$locale_object->origin = 'source';
}
else {
$query = db_select('locales_target', 'lt');
$query->join('locales_source', 'ls', 'ls.lid = lt.lid');
$query
->fields('lt')
->fields('ls')
->condition('lt.lid', $locale_lid)
->condition('lt.language', $source_language);
$locale_object = $query
->execute()
->fetchObject();
if (empty($locale_object)) {
return null;
}
$locale_object->origin = 'target';
}
return $locale_object;
}
/**
* {@inheritdoc}
*/
public function getLabel(TMGMTJobItem $job_item) {
if ($locale_object = $this->getLocaleObject($job_item)) {
if ($locale_object->origin == 'source') {
$label = $locale_object->source;
}
else {
$label = $locale_object->translation;
}
return truncate_utf8(strip_tags($label), 30, FALSE, TRUE);
}
}
/**
* [@inheritdoc}
*/
public function getType(TMGMTJobItem $job_item) {
return $this->getItemTypeLabel($job_item->item_type);
}
/**
* {@inheritdoc}
*/
public function getData(TMGMTJobItem $job_item) {
$locale_object = $this->getLocaleObject($job_item);
if (empty($locale_object)) {
$languages = language_list();
throw new TMGMTException(t('Unable to load %language translation for the locale %id',
array('%language' => $languages[$job_item->getJob()->source_language]->name, '%id' => $job_item->item_id)));
}
if ($locale_object->origin == 'source') {
$text = $locale_object->source;
}
else {
$text = $locale_object->translation;
}
// Identify placeholders that need to be escaped. Assume that placeholders
// consist of alphanumeric characters and _,- only and are delimited by
// non-alphanumeric characters. There are cases that don't match, for
// example appended SI units like "@valuems", there only @value is the
// actual placeholder.
$escape = array();
if (preg_match_all('/([@!%][a-zA-Z0-9_-]+)/', $text, $matches, PREG_OFFSET_CAPTURE)) {
foreach ($matches[0] as $match) {
$escape[$match[1]]['string'] = $match[0];
}
}
$structure['singular'] = array(
'#label' => t('Singular'),
'#text' => (string) $text,
'#translate' => TRUE,
'#escape' => $escape,
);
return $structure;
}
/**
* {@inheritdoc}
*/
public function saveTranslation(TMGMTJobItem $job_item) {
$job = tmgmt_job_load($job_item->tjid);
$data = $job_item->getData();
if (isset($data['singular'])) {
$translation = $data['singular']['#translation']['#text'];
// Update the locale string in the system.
// @todo: Send error message to user if update fails.
if ($this->updateTranslation($job_item->item_id, $job->target_language, $translation)) {
$job_item->accepted();
}
}
// @todo: Temporary backwards compability with existing jobs, remove in next
// release.
if (isset($data[$job_item->item_id])) {
$translation = $data[$job_item->item_id]['#translation']['#text'];
// Update the locale string in the system.
// @todo: Send error message to user if update fails.
if ($this->updateTranslation($job_item->item_id, $job->target_language, $translation)) {
$job_item->accepted();
}
}
}
/**
* {@inheritdoc}
*/
public function getSourceLangCode(TMGMTJobItem $job_item) {
// For the locale source English is always the source language.
return 'en';
}
/**
* {@inheritdoc}
*/
public function getExistingLangCodes(TMGMTJobItem $job_item) {
$query = db_select('locales_target', 'lt');
$query->fields('lt', array('language'));
$query->condition('lt.lid', $job_item->item_id);
$existing_lang_codes = array('en');
foreach ($query->execute() as $language) {
$existing_lang_codes[] = $language->language;
}
return $existing_lang_codes;
}
}

View File

@@ -0,0 +1,231 @@
<?php
/**
* Basic Locale Source tests.
*/
class TMGMTLocaleSourceTestCase extends TMGMTBaseTestCase {
static function getInfo() {
return array(
'name' => 'Locale Source tests',
'description' => 'Exporting source data from locale and saving translations back',
'group' => 'Translation Management',
);
}
function setUp() {
parent::setUp(array('tmgmt_locale'));
$this->langcode = 'de';
$this->context = 'default';
$file = new stdClass();
$file->uri = drupal_realpath(drupal_get_path('module', 'tmgmt_locale') . '/tests/test.xx.po');
$this->pofile = file_save($file);
$this->setEnvironment($this->langcode);
$this->setEnvironment('es');
}
/**
* Tests translation of a locale singular term.
*/
function testSingularTerm() {
// Load PO file to create a locale structure in the database.
_locale_import_po($this->pofile, $this->langcode, LOCALE_IMPORT_OVERWRITE, $this->context);
// Obtain one locale string with translation.
$locale_object = db_query('SELECT * FROM {locales_source} WHERE source = :source LIMIT 1', array(':source' => 'Hello World'))->fetchObject();
$source_text = $locale_object->source;
// Create the new job and job item.
$job = $this->createJob();
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
$item1 = $job->addItem('locale', 'default', $locale_object->lid);
// Check the structure of the imported data.
$this->assertEqual($item1->item_id, $locale_object->lid, 'Locale Strings object correctly saved');
$this->assertEqual('Locale', $item1->getSourceType());
$this->assertEqual('Hello World', $item1->getSourceLabel());
$job->requestTranslation();
foreach ($job->getItems() as $item) {
/* @var $item TMGMTJobItem */
$item->acceptTranslation();
$this->assertTrue($item->isAccepted());
// The source is now available in en and de.
$this->assertJobItemLangCodes($item, 'en', array('en', 'de'));
}
// Check string translation.
$expected_translation = $job->target_language . '_' . $source_text;
$this->assertTranslation($locale_object->lid, 'de', $expected_translation);
// Translate the german translation to spanish.
$target_langcode = 'es';
$job = $this->createJob('de', $target_langcode);
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
$item1 = $job->addItem('locale', 'default', $locale_object->lid);
$this->assertEqual('Locale', $item1->getSourceType());
$this->assertEqual($expected_translation, $item1->getSourceLabel());
$job->requestTranslation();
foreach ($job->getItems() as $item) {
/* @var $item TMGMTJobItem */
$item->acceptTranslation();
$this->assertTrue($item->isAccepted());
// The source should be now available for en, de and es languages.
$this->assertJobItemLangCodes($item, 'en', array('en', 'de', 'es'));
}
// Check string translation.
$this->assertTranslation($locale_object->lid, $target_langcode, $job->target_language . '_' . $expected_translation);
}
/**
* Test if the source is able to pull content in requested language.
*/
function testRequestDataForSpecificLanguage() {
$this->setEnvironment('cs');
_locale_import_po($this->pofile, $this->langcode, LOCALE_IMPORT_OVERWRITE, $this->context);
$locale_object = db_query('SELECT * FROM {locales_source} WHERE source = :source LIMIT 1', array(':source' => 'Hello World'))->fetchObject();
$plugin = new TMGMTLocaleSourcePluginController('locale', 'locale');
$reflection_plugin = new ReflectionClass('TMGMTLocaleSourcePluginController');
$updateTranslation = $reflection_plugin->getMethod('updateTranslation');
$updateTranslation->setAccessible(TRUE);
$updateTranslation->invoke($plugin, $locale_object->lid, 'de', 'de translation');
// Create the new job and job item.
$job = $this->createJob('de', 'cs');
$job->save();
$job->addItem('locale', 'default', $locale_object->lid);
$data = $job->getData();
$this->assertEqual($data[1]['singular']['#text'], 'de translation');
// Create new job item with a source language for which the translation
// does not exit.
$job = $this->createJob('es', 'cs');
$job->save();
try {
$job->addItem('locale', 'default', $locale_object->lid);
$this->fail('The job item should not be added as there is no translation for language "es"');
}
catch (TMGMTException $e) {
$languages = language_list();
$this->assertEqual(t('Unable to load %language translation for the locale %id',
array('%language' => $languages['es']->name, '%id' => $locale_object->lid)), $e->getMessage());
}
}
/**
* Verifies that strings that need escaping are correctly identified.
*/
function testEscaping() {
$lid = db_insert('locales_source')
->fields(array(
'source' => '@place-holders need %to be !esc_aped.',
'textgroup' => 'default',
'context' => '',
))
->execute();
$job = $this->createJob('en', 'de');
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
$item = $job->addItem('locale', 'default', $lid);
$data = $item->getData();
$expected_escape = array(
0 => array('string' => '@place-holders'),
20 => array('string' => '%to'),
27 => array('string' => '!esc_aped'),
);
$this->assertEqual($data['singular']['#escape'], $expected_escape);
// Invalid patterns that should be ignored.
$lid = db_insert('locales_source')
->fields(array(
'source' => '@ % ! example',
'textgroup' => 'default',
'context' => '',
))
->execute();
$item = $job->addItem('locale', 'default', $lid);
$data = $item->getData();
$this->assertTrue(empty($data[$lid]['#escape']));
}
/**
* Tests that system behaves correctly with an non-existing locales.
*/
function testInexistantSource() {
// Create inexistant locale object.
$locale_object = new stdClass();
$locale_object->lid = 0;
// Create the job.
$job = $this->createJob();
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
// Create the job item.
try {
$job->addItem('locale', 'default', $locale_object->lid);
$this->fail('Job item add with an inexistant locale.');
}
catch (TMGMTException $e) {
$this->pass('Exception thrown when trying to translate non-existing locale string');
}
// Try to translate a source string without translation from german to
// spanish.
$lid = db_insert('locales_source')
->fields(array(
'source' => 'No translation',
'textgroup' => 'default',
'context' => '',
))
->execute();
$job = $this->createJob('de', 'fr');
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
try {
$job->addItem('locale', 'default', $lid);
$this->fail('Job item add with an non-existing locale did not fail.');
}
catch (TMGMTException $e) {
$this->pass('Job item add with an non-existing locale did fail.');
}
}
/**
* Asserts a locale translation.
*
* @param int $lid
* The locale source id.
* @param string $target_langcode
* The target language code.
* @param string $expected_translation
* The expected translation.
*/
public function assertTranslation($lid, $target_langcode, $expected_translation) {
$actual_translation = db_query('SELECT translation FROM {locales_target} WHERE lid = :lid AND language = :language', array(
':lid' => $lid,
':language' => $target_langcode
))->fetchField();
$this->assertEqual($actual_translation, $expected_translation);
}
}

View File

@@ -0,0 +1,316 @@
<?php
/**
* @file
* Provides the I18nString source controller.
*/
/**
* Class TMGMTI18nStringDefaultSourceUIController
*
* UI Controller fo i18n strings translation jobs.
*/
class TMGMTLocaleSourceUIController extends TMGMTDefaultSourceUIController {
/**
* Gets locale strings.
*
* @param string $textgroup
* The locale textgroup.
* @param string $search_label
* Label to search for.
* @param string $missing_target_language
* Missing translation language.
*
* @return array
* List of i18n strings data.
*/
function getStrings($textgroup, $search_label = NULL, $missing_target_language = NULL) {
$languages = drupal_map_assoc(array_keys(language_list()));
$select = db_select('locales_source', 'ls')
->fields('ls', array('lid', 'source'));
$select->addTag('tmgmt_sources_search');
$select->addMetaData('plugin', 'locale');
$select->addMetaData('type', $textgroup);
$select->condition('ls.textgroup', $textgroup);
if (!empty($search_label)) {
$select->condition('ls.source', "%$search_label%", 'LIKE');
}
if (!empty($missing_target_language) && in_array($missing_target_language, $languages)) {
$select->isNull("lt_$missing_target_language.language");
}
// Join locale targets for each language.
// We want all joined fields to be named as langcodes, but langcodes could
// contain hyphens in their names, which is not allowed by the most database
// engines. So we create a langcode-to-filed_alias map, and rename fields
// later.
$langcode_to_filed_alias_map = array();
foreach ($languages as $langcode) {
$table_alias = $select->leftJoin('locales_target', db_escape_field("lt_$langcode"), "ls.lid = %alias.lid AND %alias.language = '$langcode'");
$langcode_to_filed_alias_map[$langcode] = $select->addField($table_alias, 'language');
}
$select = $select->extend('PagerDefault')->limit(variable_get('tmgmt_source_list_limit', 20));
$rows = $select->execute()->fetchAll();
foreach ($rows as $row) {
foreach ($langcode_to_filed_alias_map as $langcode => $field_alias) {
$row->{$langcode} = $row->{$field_alias};
unset($row->{$field_alias});
}
}
return $rows;
}
/**
* Gets overview form header.
*
* @return array
* Header array definition as expected by theme_tablesort().
*/
public function overviewFormHeader() {
$languages = array();
foreach (language_list() as $langcode => $language) {
$languages['langcode-' . $langcode] = array(
'data' => check_plain($language->name),
);
}
$header = array(
'source' => array('data' => t('Source text')),
) + $languages;
return $header;
}
/**
* Implements TMGMTSourceUIControllerInterface::overviewForm().
*/
public function overviewForm($form, &$form_state, $type) {
$form += $this->overviewSearchFormPart($form, $form_state, $type);
$form['items'] = array(
'#type' => 'tableselect',
'#header' => $this->overviewFormHeader($type),
'#empty' => t('No strings matching given criteria have been found.')
);
$search_data = $this->getSearchFormSubmittedParams();
$strings = $this->getStrings($type, $search_data['label'], $search_data['missing_target_language']);
foreach ($this->getTranslationData($strings, $type) as $id => $data) {
$form['items']['#options'][$id] = $this->overviewRow($type, $data);
}
$form['pager'] = array('#markup' => theme('pager', array('tags' => NULL)));
return $form;
}
/**
* Helper function to create translation data list for the sources page list.
*
* @param array $strings
* Result of the search query returned by tmgmt_i18n_string_get_strings().
* @param string $type
* I18n object type.
*
* @return array
* Structured array with translation data.
*/
protected function getTranslationData($strings, $type) {
$objects = array();
// Source language of locale strings is always english.
$source_language = 'en';
foreach ($strings as $string) {
$id = $string->lid;
// Get existing translations and current job items for the entity
// to determine translation statuses
$current_job_items = tmgmt_job_item_load_latest('locale', $type, $id, $source_language);
$objects[$id] = array(
'id' => $id,
'object' => $string
);
// Load entity translation specific data.
foreach (language_list() as $langcode => $language) {
$translation_status = 'current';
if ($langcode == $source_language) {
$translation_status = 'original';
}
elseif ($string->{$langcode} === NULL) {
$translation_status = 'missing';
}
$objects[$id]['current_job_items'][$langcode] = isset($current_job_items[$langcode]) ? $current_job_items[$langcode] : NULL;
$objects[$id]['translation_statuses'][$langcode] = $translation_status;
}
}
return $objects;
}
/**
* Builds search form for entity sources overview.
*
* @param array $form
* Drupal form array.
* @param $form_state
* Drupal form_state array.
* @param $type
* Entity type.
*
* @return array
* Drupal form array.
*/
public function overviewSearchFormPart($form, &$form_state, $type) {
$options = array();
foreach (language_list() as $langcode => $language) {
$options[$langcode] = $language->name;
}
$default_values = $this->getSearchFormSubmittedParams();
$form['search_wrapper'] = array(
'#prefix' => '<div class="tmgmt-sources-wrapper tmgmt-i18n_string-sources-wrapper">',
'#suffix' => '</div>',
'#weight' => -15,
);
$form['search_wrapper']['search'] = array(
'#tree' => TRUE,
);
$form['search_wrapper']['search']['label'] = array(
'#type' => 'textfield',
'#title' => t('Source text'),
'#default_value' => isset($default_values['label']) ? $default_values['label'] : NULL,
);
// Unset English as it is the source language for all locale strings.
unset($options['en']);
$form['search_wrapper']['search']['missing_target_language'] = array(
'#type' => 'select',
'#title' => t('Not translated to'),
'#options' => $options,
'#empty_option' => '--',
'#default_value' => isset($default_values['missing_target_language']) ? $default_values['missing_target_language'] : NULL,
);
$form['search_wrapper']['search_submit'] = array(
'#type' => 'submit',
'#value' => t('Search'),
);
return $form;
}
/**
* Gets submitted search params.
*
* @return array
*/
public function getSearchFormSubmittedParams() {
$params = array(
'label' => NULL,
'missing_target_language' => NULL,
);
if (isset($_GET['label'])) {
$params['label'] = $_GET['label'];
}
if (isset($_GET['missing_target_language'])) {
$params['missing_target_language'] = $_GET['missing_target_language'];
}
return $params;
}
/**
* Builds a table row for overview form.
*
* @param string $type
* i18n type.
* @param array $data
* Data needed to build the list row.
*
* @return array
*/
public function overviewRow($type, $data) {
// Set the default item key, assume it's the first.
$source = $data['object'];
$row = array(
'id' => $data['id'],
'source' => check_plain($source->source),
);
foreach (language_list() as $langcode => $language) {
$row['langcode-' . $langcode] = theme('tmgmt_ui_translation_language_status_single', array(
'translation_status' => $data['translation_statuses'][$langcode],
'job_item' => isset($data['current_job_items'][$langcode]) ? $data['current_job_items'][$langcode] : NULL,
));
}
return $row;
}
/**
* Implements TMGMTSourceUIControllerInterface::overviewFormSubmit().
*/
public function overviewFormSubmit($form, &$form_state, $type) {
// Handle search redirect.
$this->overviewSearchFormRedirect($form, $form_state, $type);
$items = array_filter($form_state['values']['items']);
$type = $form_state['item_type'];
$source_lang = 'en';
// Create only single job for all items as the source language is just
// the same for all.
$job = tmgmt_job_create($source_lang, NULL, $GLOBALS['user']->uid);
// Loop through entities and create individual jobs for each source language.
foreach ($items as $item) {
$job->addItem('locale', $type, $item);
}
$form_state['redirect'] = array('admin/tmgmt/jobs/' . $job->tjid,
array('query' => array('destination' => current_path())));
drupal_set_message(t('One job needs to be checked out.'));
}
/**
* Performs redirect with search params appended to the uri.
*
* In case of triggering element is edit-search-submit it redirects to
* current location with added query string containing submitted search form
* values.
*
* @param array $form
* Drupal form array.
* @param $form_state
* Drupal form_state array.
* @param $type
* Entity type.
*/
public function overviewSearchFormRedirect($form, &$form_state, $type) {
if ($form_state['triggering_element']['#id'] == 'edit-search-submit') {
$query = array();
foreach ($form_state['values']['search'] as $key => $value) {
$query[$key] = $value;
}
drupal_goto($_GET['q'], array('query' => $query));
}
}
}

View File

@@ -0,0 +1,105 @@
<?php
/**
* Basic Locale Source tests.
*/
class TMGMTLocaleSourceUiTestCase extends TMGMTBaseTestCase {
static function getInfo() {
return array(
'name' => 'Locale Source UI tests',
'description' => 'Tests the locale source overview',
'group' => 'Translation Management',
);
}
function setUp() {
parent::setUp(array('tmgmt_locale', 'tmgmt_ui'));
$this->langcode = 'de';
$this->context = 'default';
$file = new stdClass();
$file->uri = drupal_realpath(drupal_get_path('module', 'tmgmt_locale') . '/tests/test.xx.po');
$this->pofile = file_save($file);
$this->setEnvironment($this->langcode);
$this->setEnvironment('gsw-berne');
}
public function testOverview() {
// Load PO file to create a locale structure in the database.
_locale_import_po($this->pofile, $this->langcode, LOCALE_IMPORT_OVERWRITE, $this->context);
$this->loginAsTranslator();
$this->drupalGet('admin/tmgmt/sources/locale_default');
$this->assertText('Hello World');
$this->assertText('Example');
$rows = $this->xpath('//tbody/tr');
foreach ($rows as $row) {
if ($row->td[1] == 'Hello World') {
$this->assertEqual((string) $row->td[3]->div['title'], t('Translation up to date'));
$this->assertEqual((string) $row->td[4]->div['title'], t('Not translated'));
}
}
// Filter on the label.
$edit = array('search[label]' => 'Hello');
$this->drupalPost(NULL, $edit, t('Search'));
$this->assertText('Hello World');
$this->assertNoText('Example');
$locale_object = db_query('SELECT * FROM {locales_source} WHERE source = :source LIMIT 1', array(':source' => 'Hello World'))->fetchObject();
// First add source to the cart to test its functionality.
$edit = array(
'items[' . $locale_object->lid . ']' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Add to cart'));
$this->assertRaw(t('@count content source was added into the <a href="@url">cart</a>.', array('@count' => 1, '@url' => url('admin/tmgmt/cart'))));
$edit['target_language[]'] = array('gsw-berne');
$this->drupalPost('admin/tmgmt/cart', $edit, t('Request translation'));
// Assert that the job item is displayed.
$this->assertText('Hello World');
$this->assertText(t('Locale'));
$this->assertText('2');
$this->drupalPost(NULL, array('target_language' => 'gsw-berne'), t('Submit to translator'));
// Test for the translation flag title.
$this->drupalGet('admin/tmgmt/sources/locale_default');
$this->assertRaw(t('Active job item: Needs review'));
// Review and accept the job item.
$job_items = tmgmt_job_item_load_latest('locale', 'default', $locale_object->lid, 'en');
$this->drupalGet('admin/tmgmt/items/' . $job_items['gsw-berne']->tjiid);
$this->assertRaw('gsw-berne_Hello World');
$this->drupalPost(NULL, array(), t('Save as completed'));
$this->drupalGet('admin/tmgmt/sources/locale_default');
$this->assertNoRaw(t('Active job item: Needs review'));
$rows = $this->xpath('//tbody/tr');
foreach ($rows as $row) {
if ($row->td[1] == 'Hello World') {
$this->assertEqual((string) $row->td[3]->div['title'], t('Translation up to date'));
$this->assertEqual((string) $row->td[4]->div['title'], t('Translation up to date'));
}
}
// Test the missing translation filter.
$this->drupalGet('admin/tmgmt/sources/locale_default');
// Check that the source language (en) has been removed from the target language
// select box.
$elements = $this->xpath('//select[@name=:name]//option[@value=:option]', array(':name' => 'search[target_language]', ':option' => 'en'));
$this->assertTrue(empty($elements));
// Filter on the "Not translated to".
$edit = array('search[missing_target_language]' => 'gsw-berne');
$this->drupalPost(NULL, $edit, t('Search'));
// Hello World is translated to "gsw-berne" therefore it must not show up in the
// list.
$this->assertNoText('Hello World');
}
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* @file
* Hooks provided by the node source plugin for TMGMT.
*/
/**
* Alter the created node translation.
*
* @param object
* $tnode translated node
* @param object
* $node source node
* @param TMGMTJobItem
* $job_item
*/
function hook_tmgmt_before_update_node_translation_alter($tnode, $node, $job_item) {
// Always store new translations as a new revision.
$tnode->revision = 1;
}

View File

@@ -0,0 +1,26 @@
name = Content Source
description = Content Translation source plugin for the Translation Management system.
package = Translation Management
core = 7.x
dependencies[] = tmgmt
dependencies[] = tmgmt_field
dependencies[] = translation
files[] = tmgmt_node.plugin.inc
files[] = tmgmt_node.ui.inc
files[] = tmgmt_node.test
; Views integration and handlers
files[] = views/tmgmt_node.views.inc
files[] = views/handlers/tmgmt_node_handler_field_translation_language_status.inc
files[] = views/handlers/tmgmt_node_handler_field_translation_language_status_single.inc
files[] = views/handlers/tmgmt_node_handler_filter_node_translatable_types.inc
files[] = views/handlers/tmgmt_node_handler_filter_missing_translation.inc
; Information added by Drupal.org packaging script on 2016-09-21
version = "7.x-1.0-rc2+1-dev"
core = "7.x"
project = "tmgmt"
datestamp = "1474446494"

View File

@@ -0,0 +1,35 @@
<?php
/**
* @file
* Source plugin for the Translation Management system that handles nodes.
*/
/**
* Implements hook_tmgmt_source_plugin_info().
*/
function tmgmt_node_tmgmt_source_plugin_info() {
$info['node'] = array(
'label' => t('Node'),
'description' => t('Source handler for nodes.'),
'plugin controller class' => 'TMGMTNodeSourcePluginController',
'ui controller class' => 'TMGMTNodeSourceUIController',
'views controller class' => 'TMGMTNodeSourceViewsController',
'item types' => array(),
);
foreach (node_type_get_names() as $type => $name) {
if (translation_supported_type($type)) {
$info['node']['item types'][$type] = $name;
}
}
return $info;
}
/**
* Form element validator for the missing target language views field handler.
*/
function tmgmt_node_views_exposed_target_language_validate($form, &$form_state) {
if (!empty($form_state['values']['tmgmt_node_missing_translation']) && $form_state['values']['language_1'] == $form_state['values']['tmgmt_node_missing_translation']) {
form_set_error('tmgmt_node_missing_translation', t('The source and target languages must not be the same.'));
}
}

View File

@@ -0,0 +1,152 @@
<?php
/**
* @file
* Provides the node source plugin controller.
*/
class TMGMTNodeSourcePluginController extends TMGMTDefaultSourcePluginController {
/**
* {@inheritdoc}
*
* Returns the data from the fields as a structure that can be processed by
* the Translation Management system.
*/
public function getData(TMGMTJobItem $job_item) {
$node = node_load($job_item->item_id);
$source_language = $job_item->getJob()->source_language;
$languages = language_list();
// If the node language is not the same as the job source language try to
// load its translation for the job source language.
if ($node->language != $source_language) {
$translation_loaded = FALSE;
foreach (translation_node_get_translations($node->nid) as $language => $translation) {
if ($language == $source_language) {
$node = node_load($translation->nid);
$translation_loaded = TRUE;
break;
}
}
if (!$translation_loaded) {
throw new TMGMTException(t('Unable to load %language translation for the node %title',
array('%language' => $languages[$source_language]->name, '%title' => $node->title)));
}
}
$type = node_type_get_type($node);
// Get all the fields that can be translated and arrange their values into
// a specific structure.
$structure = tmgmt_field_get_source_data('node', $node, $job_item->getJob()->source_language);
$structure['node_title']['#label'] = $type->title_label;
$structure['node_title']['#text'] = $node->title;
return $structure;
}
/**
* {@inheritdoc}
*/
public function saveTranslation(TMGMTJobItem $job_item) {
if ($node = node_load($job_item->item_id)) {
$job = $job_item->getJob();
if (empty($node->tnid)) {
// We have no translation source nid, this is a new set, so create it.
$node->tnid = $node->nid;
node_save($node);
}
$translations = translation_node_get_translations($node->tnid);
if (isset($translations[$job->target_language])) {
// We have already a translation for the source node for the target
// language, so load it.
$tnode = node_load($translations[$job->target_language]->nid);
}
else {
// We don't have a translation for the source node yet, so create one.
$tnode = clone $node;
unset($tnode->nid, $tnode->vid, $tnode->uuid, $tnode->vuuid);
$tnode->language = $job->target_language;
$tnode->translation_source = $node;
}
// Allow modules and translator plugins to alter, for example in the
// case of creating revisions for translated nodes, or altering
// properties of the tnode before saving.
drupal_alter('tmgmt_before_update_node_translation', $tnode, $node, $job_item);
// Time to put the translated data into the node.
$data = $job_item->getData();
// Special case for the node title.
if (isset($data['node_title']['#translation']['#text'])) {
$tnode->title = $data['node_title']['#translation']['#text'];
unset($data['node_title']);
}
tmgmt_field_populate_entity('node', $tnode, $job->target_language, $data, FALSE);
// Reset translation field, which determines outdated status.
$tnode->translation['status'] = 0;
node_save($tnode);
// We just saved the translation, set the sate of the job item to
// 'finished'.
$job_item->accepted();
}
}
/**
* {@inheritdoc}
*/
public function getLabel(TMGMTJobItem $job_item) {
if ($node = node_load($job_item->item_id)) {
return entity_label('node', $node);
}
return parent::getLabel($job_item);
}
/**
* {@inheritdoc}
*/
public function getUri(TMGMTJobItem $job_item) {
if ($node = node_load($job_item->item_id)) {
return entity_uri('node', $node);
}
return parent::getUri($job_item);
}
/**
* {@inheritdoc}
*/
public function getType(TMGMTJobItem $job_item) {
if ($node = node_load($job_item->item_id)) {
return node_type_get_name($node);
}
return parent::getType($job_item);
}
/**
* {@inheritdoc}
*/
public function getSourceLangCode(TMGMTJobItem $job_item) {
if ($node = node_load($job_item->item_id)) {
return entity_language('node', $node);
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function getExistingLangCodes(TMGMTJobItem $job_item) {
$existing_lang_codes = array();
if ($node = node_load($job_item->item_id)) {
$existing_lang_codes = array(entity_language('node', $node));
}
if ($translations = translation_node_get_translations($job_item->item_id)) {
$existing_lang_codes = array_unique(array_merge($existing_lang_codes, array_keys($translations)));
}
return $existing_lang_codes;
}
}

View File

@@ -0,0 +1,147 @@
<?php
/**
* Basic Node Source tests.
*/
class TMGMTNodeSourceTestCase extends TMGMTEntityTestCaseUtility {
static function getInfo() {
return array(
'name' => 'Node Source tests',
'description' => 'Exporting source data from nodes and saving translations back to nodes',
'group' => 'Translation Management',
);
}
function setUp() {
parent::setUp(array('tmgmt_node', 'translation'));
$this->loginAsAdmin();
$this->setEnvironment('de');
$this->createNodeType('page', 'Basic page', TRANSLATION_ENABLED, FALSE);
$this->attachFields('node', 'page', array(TRUE, TRUE, FALSE, FALSE));
}
/**
* Tests nodes field translation.
*/
function testNodeSource() {
// Create a translation job.
$job = $this->createJob();
$job->translator = $this->default_translator->name;
$job->settings = array();
$job->save();
for ($i = 0; $i < 2; $i++) {
$node = $this->createNode('page');
// Create a job item for this node and add it to the job.
$item = $job->addItem('node', 'node', $node->nid);
$this->assertEqual('Basic page', $item->getSourceType());
}
// Translate the job.
$job->requestTranslation();
foreach ($job->getItems() as $item) {
// The source is only available in en.
$this->assertJobItemLangCodes($item, 'en', array('en'));
$item->acceptTranslation();
$node = node_load($item->item_id);
// Check if the tnid attribute is bigger than 0.
$this->assertTrue($node->tnid > 0, 'The source node is part of a translation set.');
// The translations may be statically cached, so make make sure
// to reset the cache before loading the node translations.
$cached_translations = & drupal_static('translation_node_get_translations', array());
unset($cached_translations[$node->tnid]);
// Load the translation set of the source node.
$translations = translation_node_get_translations($node->tnid);
$this->assertNotNull($translations['de'], 'Translation found for the source node.');
if (isset($translations['de'])) {
$tnode = node_load($translations['de']->nid, NULL, TRUE);
$this->checkTranslatedData($tnode, $item->getData(), 'de');
}
// The source should be now available for en and de.
$this->assertJobItemLangCodes($item, 'en', array('de', 'en'));
}
}
/**
* Test if the source is able to pull content in requested language.
*/
function testRequestDataForSpecificLanguage() {
$this->setEnvironment('sk');
$this->setEnvironment('es');
$content_type = $this->drupalCreateContentType();
$node = $this->drupalCreateNode(array(
'title' => $this->randomName(),
'language' => 'sk',
'body' => array('sk' => array(array())),
'type' => $content_type->type,
));
$this->drupalCreateNode(array(
'title' => 'en translation',
'language' => 'en',
'tnid' => $node->nid,
'body' => array('en' => array(array())),
'type' => $content_type->type,
));
// Create a translation job.
$job = $this->createJob('en', 'de');
$job->save();
$job->addItem('node', 'node', $node->nid);
$data = $job->getData();
$this->assertEqual($data[1]['node_title']['#text'], 'en translation');
// Create new job item with a source language for which the translation
// does not exit.
$job = $this->createJob('es', 'cs');
$job->save();
try {
$job->addItem('node', 'node', $node->nid);
$this->fail('The job item should not be added as there is no translation for language "es"');
}
catch (TMGMTException $e) {
$languages = language_list();
$this->assertEqual(t('Unable to load %language translation for the node %title',
array('%language' => $languages['es']->name, '%title' => $node->title)), $e->getMessage());
}
}
/**
* Compares the data from an entity with the translated data.
*
* @param $node
* The translated node object.
* @param $data
* An array with the translated data.
* @param $langcode
* The code of the target language.
*/
function checkTranslatedData($node, $data, $langcode) {
foreach (element_children($data) as $field_name) {
if ($field_name == 'node_title') {
$this->assertEqual($node->title, $data['node_title']['#translation']['#text'], 'The title of the translated node matches the translated data.');
continue;
}
foreach (element_children($data[$field_name]) as $delta) {
$field_langcode = field_is_translatable('node', field_info_field($field_name)) ? $langcode : LANGUAGE_NONE;
foreach (element_children($data[$field_name][$delta]) as $column) {
$column_value = $data[$field_name][$delta][$column];
if (!isset($column_value['#translate']) || $column_value['#translate']) {
$this->assertEqual($node->{$field_name}[$field_langcode][$delta][$column], $column_value['#translation']['#text'], format_string('The translatable field %field:%delta has been populated with the proper translated data.', array(
'%field' => $field_name,
'delta' => $delta
)));
}
}
}
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* @file
* Provides the node source ui controller.
*/
class TMGMTNodeSourceUIController extends TMGMTDefaultSourceUIController {
/**
* {@inheritdoc}
*/
public function hook_menu() {
// The node source overview is a View using Views Bulk Operations. Therefore
// we don't need to provide any menu items.
return array();
}
/**
* {@inheritdoc}
*/
public function hook_forms() {
// The node source overview is a View using Views Bulk Operations. Therefore
// we don't need to provide any forms.
return array();
}
/**
* {@inheritdoc}
*/
public function hook_views_default_views() {
return _tmgmt_load_exports('tmgmt_node', 'views', 'view.inc', 'view');
}
}

View File

@@ -0,0 +1,22 @@
name = Content Source User Interface
description = User Interface for the content translation source plugin.
package = Translation Management
core = 7.x
dependencies[] = tmgmt_node
dependencies[] = tmgmt_ui
dependencies[] = views_bulk_operations
files[] = tmgmt_node_ui.test
files[] = tmgmt_node_ui.overview.test
; Views handlers
files[] = views/tmgmt_node_ui_handler_filter_node_translatable_types.inc
files[] = views/tmgmt_node_ui_handler_field_jobs.inc
; Information added by Drupal.org packaging script on 2016-09-21
version = "7.x-1.0-rc2+1-dev"
core = "7.x"
project = "tmgmt"
datestamp = "1474446494"

View File

@@ -0,0 +1,93 @@
<?php
/**
* @file
* Main module file for the translation management node source plugin user
* interface.
*/
/**
* Implements hook_page_alter().
*/
function tmgmt_node_ui_page_alter(&$page) {
// Translation tabs for nodes.
if (($node = menu_get_object()) && entity_access('create', 'tmgmt_job')) {
if (isset($page['content']['system_main']['translation_node_overview'])) {
module_load_include('inc', 'tmgmt_node_ui', 'tmgmt_node_ui.pages');
$page['content']['system_main']['translation_node_overview'] = drupal_get_form('tmgmt_node_ui_node_form', $node, $page['content']['system_main']['translation_node_overview']);
}
// Support the context module: when context is used for placing the
// system_main block, then block contents appear nested one level deeper
// under another 'content' key.
elseif (isset($page['content']['system_main']['content']['translation_node_overview'])) {
module_load_include('inc', 'tmgmt_node_ui', 'tmgmt_node_ui.pages');
$page['content']['system_main']['content']['translation_node_overview'] = drupal_get_form('tmgmt_node_ui_node_form', $node, $page['content']['system_main']['content']['translation_node_overview']);
}
}
}
/**
* Implements hook_action_info().
*/
function tmgmt_node_ui_action_info() {
return array(
'tmgmt_node_ui_checkout_multiple_action' => array(
'type' => 'node',
'label' => t('Request translations'),
'configurable' => false,
'aggregate' => true
)
);
}
/**
* Action to do multistep checkout for translations.
*
* @param array $nodes
* Array of Drupal nodes.
* @param $info
* Action info - not used.
*
*/
function tmgmt_node_ui_checkout_multiple_action($nodes, $info) {
$jobs = array();
$source_lang_registry = array();
// Loop through entities and create individual jobs for each source language.
foreach ($nodes as $node) {
try {
// For given source lang no job exists yet.
if (!isset($source_lang_registry[$node->language])) {
// Create new job.
$job = tmgmt_job_create($node->language, NULL, $GLOBALS['user']->uid);
// Add initial job item.
$job->addItem('node', 'node', $node->nid);
// Add job identifier into registry
$source_lang_registry[$node->language] = $job->tjid;
// Add newly created job into jobs queue.
$jobs[$job->tjid] = $job;
}
// We have a job for given source lang, so just add new job item for the
// existing job.
else {
$jobs[$source_lang_registry[$node->language]]->addItem('node', 'node', $node->nid);
}
}
catch (TMGMTException $e) {
watchdog_exception('tmgmt', $e);
drupal_set_message(t('Unable to add job item for node %name. Make sure the source content is not empty.', array('%name' => $node->title)), 'error');
}
}
// If necessary, do a redirect.
$redirects = tmgmt_ui_job_checkout_multiple($jobs);
if ($redirects) {
tmgmt_ui_redirect_queue_set($redirects, current_path());
drupal_set_message(format_plural(count($redirects), t('One job needs to be checked out.'), t('@count jobs need to be checked out.')));
drupal_goto(tmgmt_ui_redirect_queue_dequeue());
}
}

View File

@@ -0,0 +1,160 @@
<?php
/**
* Content Overview Tests
*/
class TMGMTNodeSourceUIOverviewTestCase extends TMGMTEntityTestCaseUtility {
static function getInfo() {
return array(
'name' => 'Node Source UI Overview tests',
'description' => 'Tests the user interface for node overviews.',
'group' => 'Translation Management',
'dependencies' => array('rules'),
);
}
function setUp() {
parent::setUp(array('tmgmt_node_ui'));
$this->loginAsAdmin();
$this->setEnvironment('de');
$this->setEnvironment('fr');
$this->setEnvironment('es');
$this->setEnvironment('el');
$this->createNodeType('page', 'Page', TRANSLATION_ENABLED, FALSE);
// 1 means that the node type can have a language but is not translatable.
$this->createNodeType('untranslated', 'Untranslated', 1, FALSE);
$this->checkPermissions(array(), TRUE);
// Allow auto-accept.
$default_translator = tmgmt_translator_load('test_translator');
$default_translator->settings = array(
'auto_accept' => TRUE,
);
$default_translator->save();
}
/**
* Tests translating through the content source overview.
*/
function testNodeSourceOverview() {
// Login as translator to translate nodes.
$this->loginAsTranslator(array(
'translate content',
'edit any page content',
'create page content',
));
// Create a bunch of english nodes.
$node1 = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
$node2 = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
$node3 = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
$node4 = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
// Create a node with an undefined language.
$node5 = $this->drupalCreateNode(array('type' => 'page'));
// Create a node of an untranslatable content type.
$node6 = $this->drupalCreateNode(array('type' => 'untranslated', 'language' => 'en', 'body' => array('en' => array(array()))));
// Go to the overview page and make sure the nodes are there.
$this->drupalGet('admin/tmgmt/sources/node');
// Make sure that valid nodes are shown.
$this->assertText($node1->title);
$this->assertText($node2->title);
$this->assertText($node3->title);
$this->assertText($node4->title);
// Nodes without a language must not be shown.
$this->assertNoText($node5->title);
// Node with a type that is not enabled for translation must not be shown.
$this->assertNoText($node6->title);
// Now translate them.
$edit = array(
'views_bulk_operations[0]' => TRUE,
'views_bulk_operations[1]' => TRUE,
'views_bulk_operations[2]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translations'));
// Some assertions on the submit form.
$this->assertText(t('@title and 2 more (English to ?, Unprocessed)', array('@title' => $node1->title)));
$this->assertText($node1->title);
$this->assertText($node2->title);
$this->assertText($node3->title);
$this->assertNoText($node4->title);
// Translate
$edit = array(
'target_language' => 'de',
);
$this->drupalPost(NULL, $edit, t('Submit to translator'));
$this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node1->title, '@language' => t('German'))));
$this->assertText(t('The translation for @title has been accepted.', array('@title' => $node1->title)));
$this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node2->title, '@language' => t('German'))));
$this->assertText(t('The translation for @title has been accepted.', array('@title' => $node1->title)));
$this->assertNoText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node3->title, '@language' => t('German'))));
$this->assertText(t('The translation for @title has been accepted.', array('@title' => $node1->title)));
// Check the translated node.
$this->clickLink($node1->title);
$this->clickLink(t('Translate'));
$this->assertText('de_' . $node1->title);
// Test for the source list limit set in the views export.
$view = views_get_view('tmgmt_node_source_overview');
$view->execute_display('default');
$this->assertEqual($view->get_items_per_page(), variable_get('tmgmt_source_list_limit', 20));
// Test the missing translation filter.
// Create nodes needed to test the missing translation filter here so that
// VBO order is not affected.
$node_not_translated = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
$node_de = $this->drupalCreateNode(array('type' => 'page', 'language' => 'de', 'body' => array('de' => array(array()))));
$this->drupalGet('admin/tmgmt/sources/node');
$this->assertText($node1->title);
$this->assertText($node_not_translated->title);
$this->assertText($node_de->title);
// Submitting the search form will not work. After the form submission the
// page does gets redirected to url without query parameters. So we simply
// access the page with desired query.
$this->drupalGet('admin/tmgmt/sources/node', array('query' => array(
'tmgmt_node_missing_translation' => 'de',
'target_status' => 'untranslated',
)));
$this->assertNoText($node1->title);
$this->assertText($node_not_translated->title);
$this->assertNoText($node_de->title);
// Update the the translate flag of the translated node and test if it is
// listed among sources with missing translation.
db_update('node')->fields(array('translate' => 1))
->condition('nid', $node1->nid)->execute();
$this->drupalGet('admin/tmgmt/sources/node', array('query' => array(
'tmgmt_node_missing_translation' => 'de',
'target_status' => 'outdated',
)));
$this->assertText($node1->title);
$this->assertNoText($node_not_translated->title);
$this->assertNoText($node_de->title);
$this->drupalGet('admin/tmgmt/sources/node', array('query' => array(
'tmgmt_node_missing_translation' => 'de',
'target_status' => 'untranslated_or_outdated',
)));
$this->assertText($node1->title);
$this->assertText($node_not_translated->title);
$this->assertNoText($node_de->title);
}
}

View File

@@ -0,0 +1,116 @@
<?php
/**
* @file
* Provides page and form callbacks for the Translation Management Tool Node
* Source User Interface module.
*/
/**
* Node translation overview form. This form overrides the Drupal core or i18n
* Content Translation page with a tableselect form.
*/
function tmgmt_node_ui_node_form($form, &$form_state, $node, $original) {
// Store the node in the form state so we can easily create the job in the
// submit handler.
$form_state['node'] = $node;
$form['top_actions']['#type'] = 'actions';
$form['top_actions']['#weight'] = -10;
tmgmt_ui_add_cart_form($form['top_actions'], $form_state, 'node', 'node', $node->nid);
// Inject our additional column into the header.
array_splice($original['#header'], -1, 0, array(t('Pending Translations')));
// Make this a tableselect form.
$form['languages'] = array(
'#type' => 'tableselect',
'#header' => $original['#header'],
'#options' => array(),
);
$languages = module_exists('i18n_node') ? i18n_node_language_list($node) : language_list();
// Check if there is a job / job item that references this translation.
$items = tmgmt_job_item_load_latest('node', 'node', $node->nid, $node->language);
foreach ($languages as $langcode => $language) {
if ($langcode == LANGUAGE_NONE) {
// Never show language neutral on the overview.
continue;
}
// Since the keys are numeric and in the same order we can shift one element
// after the other from the original non-form rows.
$option = array_shift($original['#rows']);
if ($langcode == $node->language) {
$additional = '<strong>' . t('Source') . '</strong>';
// This is the source object so we disable the checkbox for this row.
$form['languages'][$langcode] = array(
'#type' => 'checkbox',
'#disabled' => TRUE,
);
}
elseif (isset($items[$langcode])) {
/** @var TMGMTJobItem $item */
$item = $items[$langcode];
if ($item->getJob()->isUnprocessed()) {
$uri = $item->getJob()->uri();
$additional = l(t('Unprocessed'), $uri['path']);
}
else {
$wrapper = entity_metadata_wrapper('tmgmt_job_item', $item);
$uri = $item->uri();
$additional = l($wrapper->state->label(), $uri['path']);
}
// Disable the checkbox for this row since there is already a translation
// in progress that has not yet been finished. This way we make sure that
// we don't stack multiple active translations for the same item on top
// of each other.
$form['languages'][$langcode] = array(
'#type' => 'checkbox',
'#disabled' => TRUE,
);
}
else {
// There is no translation job / job item for this target language.
$additional = t('None');
}
// Inject the additional column into the array.
array_splice($option, -1, 0, array($additional));
// Append the current option array to the form.
$form['languages']['#options'][$langcode] = $option;
}
$form['actions']['#type'] = 'actions';
$form['actions']['request'] = array(
'#type' => 'submit',
'#value' => t('Request translation'),
'#submit' => array('tmgmt_node_ui_translate_form_submit'),
'#validate' => array('tmgmt_node_ui_translate_form_validate'),
);
return $form;
}
/**
* Validation callback for the node translation overview form.
*/
function tmgmt_node_ui_translate_form_validate($form, &$form_state) {
$selected = array_filter($form_state['values']['languages']);
if (empty($selected)) {
form_set_error('languages', t('You have to select at least one language for requesting a translation.'));
}
}
/**
* Submit callback for the node translation overview form.
*/
function tmgmt_node_ui_translate_form_submit($form, &$form_state) {
$node = $form_state['node'];
$values = $form_state['values'];
$jobs = array();
foreach (array_keys(array_filter($values['languages'])) as $langcode) {
// Create the job object.
$job = tmgmt_job_create($node->language, $langcode, $GLOBALS['user']->uid);
// Add the job item.
$job->addItem('node', 'node', $node->nid);
// Append this job to the array of created jobs so we can redirect the user
// to a multistep checkout form if necessary.
$jobs[$job->tjid] = $job;
}
tmgmt_ui_job_checkout_and_redirect($form_state, $jobs);
}

View File

@@ -0,0 +1,49 @@
<?php
/*
* @file
* Contains default rules.
*/
/**
* Implements hook_default_rules_configuration().
*/
function tmgmt_node_ui_default_rules_configuration() {
$data = '{ "tmgmt_node_ui_request_translation" : {
"LABEL" : "Request translation",
"PLUGIN" : "rule",
"REQUIRES" : [ "tmgmt" ],
"USES VARIABLES" : { "nodes" : { "label" : "Nodes", "type" : "list\u003Cnode\u003E" } },
"DO" : [
{ "tmgmt_get_first_from_node_list" : {
"USING" : { "list" : [ "nodes" ] },
"PROVIDE" : { "first_node" : { "first_node" : "Node" } }
}
},
{ "tmgmt_rules_create_job" : {
"USING" : { "source_language" : [ "first-node:language" ] },
"PROVIDE" : { "job" : { "job" : "Job" } }
}
},
{ "LOOP" : {
"USING" : { "list" : [ "nodes" ] },
"ITEM" : { "node" : "Node" },
"DO" : [
{ "tmgmt_rules_job_add_item" : {
"job" : [ "job" ],
"plugin" : "node",
"item_type" : "node",
"item_id" : [ "node:nid" ]
}
}
]
}
},
{ "tmgmt_rules_job_checkout" : { "job" : [ "job" ] } }
]
}
}';
$rule = rules_import($data);
$configs[$rule->name] = $rule;
return $configs;
}

View File

@@ -0,0 +1,15 @@
#edit-tmgmt-node-missing-translation-wrapper .form-item-target-status,
#edit-tmgmt-node-missing-translation-wrapper .form-item-tmgmt-node-missing-translation {
float: left;
}
#edit-tmgmt-node-missing-translation-wrapper .form-item-target-status {
margin: 0 0 0 55px;
padding: 0;
position: relative;
top: -19px;
}
#edit-tmgmt-node-missing-translation-wrapper .form-item-target-status select {
margin-top: 9px;
}

View File

@@ -0,0 +1,450 @@
<?php
/**
* Basic Node Source UI tests.
*/
class TMGMTNodeSourceUITestCase extends TMGMTEntityTestCaseUtility {
static function getInfo() {
return array(
'name' => 'Node Source UI tests',
'description' => 'Tests the user interface for node translation sources.',
'group' => 'Translation Management',
);
}
function setUp() {
parent::setUp(array('tmgmt_node_ui', 'block'));
// We need the administer blocks permission.
$this->loginAsAdmin(array('administer blocks'));
$this->setEnvironment('de');
$this->setEnvironment('fr');
$this->setEnvironment('es');
$this->setEnvironment('el');
// @todo Re-enable this when switching to testing profile.
// Enable the main page content block for hook_page_alter() to work.
$edit = array(
'blocks[system_main][region]' => 'content',
);
$this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
$this->createNodeType('page', 'Page', TRANSLATION_ENABLED, FALSE);
}
/**
* Tests the create, submit and accept permissions.
*/
function testPermissions() {
$no_permissions = $this->drupalCreateUser();
$this->drupalLogin($no_permissions);
$this->drupalGet('admin/tmgmt');
$this->assertResponse(403);
// Test with a user that is only allowed to create jobs.
$create_user = $this->drupalCreateUser(array('access administration pages', 'translate content', 'create translation jobs'));
$this->drupalLogin($create_user);
// Create an english source node.
$node = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
// Go to the translate tab.
$this->drupalGet('node/' . $node->nid);
$this->clickLink('Translate');
// Request a translation for german.
$edit = array(
'languages[de]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
$this->assertText(t('One job has been created.'));
// Verify that we are still on the translate tab.
$this->assertText(t('Translations of @title', array('@title' => $node->title)));
// The job is unprocessed, check the status flag in the source list.
$this->drupalGet('admin/tmgmt/sources');
$links = $this->xpath('//a[contains(@title, :title)]', array(':title' => t('Active job item: @state', array('@state' => t('Unprocessed')))));
$attributes = $links[0]->attributes();
// Check if the found link points to the job checkout page instead of the
// job item review form.
$this->assertEqual($attributes['href'], url('admin/tmgmt/jobs/1', array('query' => array('destination' => 'admin/tmgmt/sources'))));
$this->drupalGet('admin/tmgmt');
$this->assertResponse(200);
$this->assertLink(t('manage'));
$this->assertNoLink(t('submit'));
$this->assertNoLink(t('delete'));
$this->assertText(t('@title', array('@title' => $node->title)));
$this->clickLink(t('manage'));
$this->assertResponse(200);
$this->assertNoRaw(t('Submit to translator'));
// Try to access the delete page directly.
$this->drupalGet($this->getUrl() . '/delete');
$this->assertResponse(403);
// Log in as user with only submit permission.
$submit_user = $this->drupalCreateUser(array('access administration pages', 'translate content', 'submit translation jobs'));
$this->drupalLogin($submit_user);
// Go to the translate tab, verify that there is no request translation
// button.
$this->drupalGet('node/' . $node->nid);
$this->clickLink('Translate');
$this->assertNoRaw(t('Request translation'));
// Go to the overview and submit the job.
$this->drupalGet('admin/tmgmt');
$this->assertResponse(200);
$this->assertLink(t('submit'));
$this->assertNoLink(t('manage'));
$this->assertNoLink(t('delete'));
$this->assertText(t('@title', array('@title' => $node->title)));
// Check VBO actions - "submit translation job" has the right to cancel
// translation only.
$element = $this->xpath('//select[@id=:id]/option/@value', array(':id' => 'edit-operation'));
$options = array();
foreach ($element as $option) {
$options[] = (string) $option;
}
$this->assertTrue(in_array('rules_component::rules_tmgmt_job_abort_translation', $options));
// Go to the job checkout page and submit it.
$this->clickLink('submit');
$this->drupalPost(NULL, array(), t('Submit to translator'));
// After submit the redirect goes back to the job overview.
$this->assertUrl('admin/tmgmt');
// Make sure that the job is active now.
$this->assertText(t('Active'));
// Click abort link and check if we are at the job abort confirm page.
$this->clickLink(t('abort'));
$this->assertText(t('This will send a request to the translator to abort the job. After the action the job translation process will be aborted and only remaining action will be resubmitting it.'));
// Return back to job overview and test the manage link.
$this->drupalGet('admin/tmgmt');
$this->clickLink(t('manage'));
$this->assertText(t('Needs review'));
$this->assertNoLink(t('review'));
// Now log in as user with only accept permission and review the job.
$accept_user = $this->drupalCreateUser(array('access administration pages', 'accept translation jobs'));
$this->drupalLogin($accept_user);
$this->drupalGet('admin/tmgmt');
// Check VBO actions - "accept translation jobs" has the right to accept
// translation only.
$element = $this->xpath('//select[@id=:id]/option/@value', array(':id' => 'edit-operation'));
$options = array();
foreach ($element as $option) {
$options[] = (string) $option;
}
$this->assertTrue(in_array('rules_component::rules_tmgmt_job_accept_translation', $options));
$this->clickLink('manage');
$this->clickLink('review');
$this->drupalPost(NULL, array(), '✓');
// Verify that the accepted character is shown.
$this->assertText('&#x2611;');
$this->drupalPost(NULL, array(), t('Save as completed'));
$this->assertText(t('Accepted'));
$this->assertText('1/0/0');
$create_user = $this->loginAsAdmin();
$this->drupalLogin($create_user);
$this->drupalGet('admin/tmgmt');
// Check VBO actions - "administer tmgmt" has rights for all actions.
$element = $this->xpath('//select[@id=:id]/option/@value', array(':id' => 'edit-operation'));
$options = array();
foreach ($element as $option) {
$options[] = (string) $option;
}
$this->assertTrue(in_array('rules_component::rules_tmgmt_job_accept_translation', $options));
$this->assertTrue(in_array('rules_component::rules_tmgmt_job_abort_translation', $options));
$this->assertTrue(in_array('rules_component::rules_tmgmt_job_delete', $options));
// Go to the translate tab, verify that there is no request translation
// button.
//$this->drupalGet('node/' . $node->nid);
//$this->clickLink('Translate');
//$this->assertNoRaw(t('Request translation'));
}
/**
* Test the translate tab for a single checkout.
*/
function testTranslateTabSingleCheckout() {
// Create a user that is allowed to translate nodes.
$translater = $this->drupalCreateUser(array('translate content', 'create translation jobs', 'submit translation jobs', 'accept translation jobs'));
$this->drupalLogin($translater);
// Create an english source node.
$node = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
// Go to the translate tab.
$this->drupalGet('node/' . $node->nid);
$this->clickLink('Translate');
// Assert some basic strings on that page.
$this->assertText(t('Translations of @title', array('@title' => $node->title)));
$this->assertText(t('Pending Translations'));
// Request a translation for german.
$edit = array(
'languages[de]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
// Verify that we are on the translate tab.
$this->assertText(t('One job needs to be checked out.'));
$this->assertText($node->title);
// Go to the translate tab and check if the pending translation label is
// "Unprocessed" and links to the job checkout page.
$this->drupalGet('node/' . $node->nid . '/translate');
$this->assertLink(t('Unprocessed'));
$this->clickLink(t('Unprocessed'));
// Submit.
$this->drupalPost(NULL, array(), t('Submit to translator'));
// Make sure that we're back on the translate tab.
$this->assertEqual(url('node/' . $node->nid . '/translate', array('absolute' => TRUE)), $this->getUrl());
$this->assertText(t('Test translation created.'));
$this->assertText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node->title, '@language' => t('German'))));
// Review.
$this->clickLink(t('Needs review'));
// @todo Review job throuh the UI.
$items = tmgmt_job_item_load_latest('node', 'node', $node->nid, 'en');
$items['de']->acceptTranslation();
// German node should now be listed and be clickable.
$this->drupalGet('node/' . $node->nid . '/translate');
$this->clickLink('de_' . $node->title);
// Test that the destination query argument does not break the redirect
// and we are redirected back to the correct page.
$this->drupalGet('node/' . $node->nid . '/translate', array('query' => array('destination' => 'node')));
// Request a spanish translation.
$edit = array(
'languages[es]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
// Verify that we are on the checkout page.
$this->assertText(t('One job needs to be checked out.'));
$this->assertText($node->title);
$this->drupalPost(NULL, array(), t('Submit to translator'));
// Make sure that we're back on the originally defined destination URL.
$this->assertEqual(url('node', array('absolute' => TRUE)), $this->getUrl());
}
/**
* Test the translate tab for a single checkout.
*/
function testTranslateTabMultipeCheckout() {
// Create a user that is allowed to translate nodes.
$translater = $this->drupalCreateUser(array('translate content', 'create translation jobs', 'submit translation jobs', 'accept translation jobs'));
$this->drupalLogin($translater);
// Create an english source node.
$node = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
// Go to the translate tab.
$this->drupalGet('node/' . $node->nid);
$this->clickLink('Translate');
// Assert some basic strings on that page.
$this->assertText(t('Translations of @title', array('@title' => $node->title)));
$this->assertText(t('Pending Translations'));
// Request a translation for german.
$edit = array(
'languages[de]' => TRUE,
'languages[es]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
// Verify that we are on the translate tab.
$this->assertText(t('2 jobs need to be checked out.'));
// Submit all jobs.
$this->assertText($node->title);
$this->drupalPost(NULL, array(), t('Submit to translator and continue'));
$this->assertText($node->title);
$this->drupalPost(NULL, array(), t('Submit to translator'));
// Make sure that we're back on the translate tab.
$this->assertEqual(url('node/' . $node->nid . '/translate', array('absolute' => TRUE)), $this->getUrl());
$this->assertText(t('Test translation created.'));
$this->assertText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node->title, '@language' => t('Spanish'))));
// Review.
$this->clickLink(t('Needs review'));
// @todo Review job throuh the UI.
$items = tmgmt_job_item_load_latest('node', 'node', $node->nid, 'en');
$items['de']->acceptTranslation();
$items['es']->acceptTranslation();
// Translated nodes should now be listed and be clickable.
$this->drupalGet('node/' . $node->nid . '/translate');
$this->clickLink('de_' . $node->title);
// Translated nodes should now be listed and be clickable.
$this->drupalGet('node/' . $node->nid . '/translate');
$this->clickLink('es_' . $node->title);
}
/**
* Test the translate tab for a single checkout.
*/
function testTranslateTabAutomatedCheckout() {
// Hide settings on the test translator.
$default_translator = tmgmt_translator_load('test_translator');
$default_translator->settings = array(
'expose_settings' => FALSE,
);
$default_translator->save();
// Create a user that is allowed to translate nodes.
$translater = $this->drupalCreateUser(array('translate content', 'create translation jobs', 'submit translation jobs', 'accept translation jobs'));
$this->drupalLogin($translater);
// Create an english source node.
$node = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
// Go to the translate tab.
$this->drupalGet('node/' . $node->nid);
$this->clickLink('Translate');
// Assert some basic strings on that page.
$this->assertText(t('Translations of @title', array('@title' => $node->title)));
$this->assertText(t('Pending Translations'));
// Request a translation for german.
$edit = array(
'languages[de]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
// Verify that we are on the translate tab.
$this->assertNoText(t('One job needs to be checked out.'));
// Make sure that we're back on the translate tab.
$this->assertEqual(url('node/' . $node->nid . '/translate', array('absolute' => TRUE)), $this->getUrl());
$this->assertText(t('Test translation created.'));
$this->assertText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node->title, '@language' => t('German'))));
// Review.
$this->clickLink(t('Needs review'));
// @todo Review job throuh the UI.
$items = tmgmt_job_item_load_latest('node', 'node', $node->nid, 'en');
$items['de']->acceptTranslation();
// German node should now be listed and be clickable.
$this->drupalGet('node/' . $node->nid . '/translate');
$this->clickLink('de_' . $node->title);
}
/**
* Test the translate tab for a single checkout.
*/
function testTranslateTabDisabledQuickCheckout() {
variable_set('tmgmt_quick_checkout', FALSE);
// Hide settings on the test translator.
$default_translator = tmgmt_translator_load('test_translator');
$default_translator->settings = array(
'expose_settings' => FALSE,
);
$default_translator->save();
// Create a user that is allowed to translate nodes.
$translater = $this->drupalCreateUser(array('translate content', 'create translation jobs', 'submit translation jobs', 'accept translation jobs'));
$this->drupalLogin($translater);
// Create an english source node.
$node = $this->drupalCreateNode(array('type' => 'page', 'language' => 'en', 'body' => array('en' => array(array()))));
// Go to the translate tab.
$this->drupalGet('node/' . $node->nid);
$this->clickLink('Translate');
// Assert some basic strings on that page.
$this->assertText(t('Translations of @title', array('@title' => $node->title)));
$this->assertText(t('Pending Translations'));
// Request a translation for german.
$edit = array(
'languages[de]' => TRUE,
);
$this->drupalPost(NULL, $edit, t('Request translation'));
// Verify that we are on the translate tab.
$this->assertText(t('One job needs to be checked out.'));
$this->assertText($node->title);
// Submit.
$this->drupalPost(NULL, array(), t('Submit to translator'));
// Make sure that we're back on the translate tab.
$this->assertEqual(url('node/' . $node->nid . '/translate', array('absolute' => TRUE)), $this->getUrl());
$this->assertText(t('Test translation created.'));
$this->assertText(t('The translation of @title to @language is finished and can now be reviewed.', array('@title' => $node->title, '@language' => t('German'))));
// Review.
$this->clickLink(t('Needs review'));
// @todo Review job throuh the UI.
$items = tmgmt_job_item_load_latest('node', 'node', $node->nid, 'en');
$items['de']->acceptTranslation();
// German node should now be listed and be clickable.
$this->drupalGet('node/' . $node->nid . '/translate');
$this->clickLink('de_' . $node->title);
}
/**
* Test the node source specific cart functionality.
*/
function testCart() {
$nodes = array();
for ($i = 0; $i < 4; $i++) {
$nodes[] = $this->createNode('page');
}
$this->loginAsAdmin(array_merge($this->translator_permissions, array('translate content')));
// Test the source overview.
$this->drupalPost('admin/tmgmt/sources/node', array(
'views_bulk_operations[0]' => TRUE,
'views_bulk_operations[1]' => TRUE,
), t('Add to cart'));
$this->drupalGet('admin/tmgmt/cart');
$this->assertText($nodes[0]->title);
$this->assertText($nodes[1]->title);
// Test the translate tab.
$this->drupalGet('node/' . $nodes[3]->nid . '/translate');
$this->assertRaw(t('There are @count items in the <a href="@url">translation cart</a>.',
array('@count' => 2, '@url' => url('admin/tmgmt/cart'))));
$this->drupalPost(NULL, array(), t('Add to cart'));
$this->assertRaw(t('@count content source was added into the <a href="@url">cart</a>.', array('@count' => 1, '@url' => url('admin/tmgmt/cart'))));
$this->assertRaw(t('There are @count items in the <a href="@url">translation cart</a> including the current item.',
array('@count' => 3, '@url' => url('admin/tmgmt/cart'))));
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* Field handler which shows all jobs which contains a node.
*
* @TODO: This could probably abstracted into a more generic handler,
* or even api function.
*/
class tmgmt_node_ui_handler_field_jobs extends views_handler_field_prerender_list {
function pre_render(&$values) {
$nids = array();
foreach ($values as $row) {
$nid = $this->get_value($row);
$nids[] = $nid;
}
$select = db_select('tmgmt_job', 'tj');
$select->join('tmgmt_job_item', 'tji', "tj.id = %alias.tjid");
$select->join('node', 'n', "tji.item_type = 'node' AND tji.plugin = 'node' AND tji.item_id = node.nid");
$select->addField('n', 'nid');
$select->addExpression('MAX(tj.id)');
dpq($select);
}
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* Field handler to display the status of all languages.
*
* @ingroup views_field_handlers
*/
class tmgmt_node_handler_field_translation_language_status extends views_handler_field {
/**
* @var views_plugin_query_default
*/
var $query;
/**
* @var array
*/
public $language_items;
/**
* Array of active job items.
*
* @var array
*/
public $active_job_items = array();
function init(&$view, &$options) {
parent::init($view, $options);
$this->view->init_style();
$this->additional_fields['nid'] = 'nid';
/**
* Dynamically add new fields so they are used
*/
$languages = language_list('language');
foreach ($languages as $langcode => $lang_info) {
$handler = views_get_handler($this->table, $this->field . '_single', 'field');
if ($handler) {
$id = $options['id'] . '_single_' . $langcode;
$this->view->display_handler->handlers['field'][$id] = $handler;
$info = array(
'id' => $id,
'table' => $this->table,
'field' => $this->field . '_single',
'label' => $lang_info->name,
);
$handler->langcode = $langcode;
$handler->main_field = $options['id'];
$handler->init($this->view, $info);
$this->language_handlers[$langcode] = $handler;
}
}
}
function pre_render(&$values) {
$nids = array();
foreach ($values as $value) {
$tnid = $this->get_value($value);
$tnid = !empty($tnid) ? $tnid : $this->get_value($value, 'nid');
$this->active_job_items[$tnid] = tmgmt_job_item_load_latest('node', 'node', $tnid, $value->node_language);
$nids[] = $tnid;
}
if ($nodes = node_load_multiple($nids)) {
$result = db_select('node', 'n')
->fields('n', array('tnid', 'language', 'translate'))
->condition('tnid', $nids)
->execute()
->fetchAll();
$this->language_items = array();
foreach ($result as $tnode) {
// The translate flag is set if the translation node is outdated, revert
// to have FALSE for outdated translations.
$this->language_items[$tnode->tnid][$tnode->language] = !$tnode->translate;
}
}
}
}

View File

@@ -0,0 +1,62 @@
<?php
/**
* @todo What is this?
*/
class tmgmt_node_handler_field_translation_language_status_single extends views_handler_field {
/**
* @var tmgmt_node_handler_field_translation_language_status
*/
var $main_handler;
/**
* @var string
*/
var $langcode;
function init(&$view, &$options) {
parent::init($view, $options);
$this->additional_fields['nid'] = array(
'table' => 'node',
'field' => 'nid',
);
}
function render($values) {
$nid = $values->nid;
$langcode = $this->langcode;
// Check if this is the source language.
if ($langcode == $values->node_language) {
$translation_status = 'original';
}
// Check if there is a translation.
elseif (!isset($this->view->field[$this->main_field]->language_items[$nid][$langcode])) {
$translation_status = 'missing';
}
// Check if the translation is outdated.
elseif (!$this->view->field[$this->main_field]->language_items[$nid][$langcode]) {
$translation_status = 'outofdate';
}
else {
$translation_status = 'current';
}
$job_item = NULL;
if (isset($this->view->field[$this->main_field]->active_job_items[$nid][$langcode])) {
$job_item = $this->view->field[$this->main_field]->active_job_items[$nid][$langcode];
}
return theme('tmgmt_ui_translation_language_status_single', array(
'translation_status' => $translation_status,
'job_item' => $job_item,
));
}
function query() {
$this->add_additional_fields();
}
}

View File

@@ -0,0 +1,108 @@
<?php
/**
* @file
* Definition of tmgmt_node_handler_filter_missing_translation.
*/
/**
* Filter by language.
*
* @ingroup views_filter_handlers
*/
class tmgmt_node_handler_filter_missing_translation extends views_handler_filter {
/**
* The target status to use for the query.
*
* @var string
*/
protected $target_status = 'untranslated_or_outdated';
/**
* {@inheritdoc}
*/
function query() {
$this->ensure_my_table();
// Don't do anything if no language was selected.
if (!$this->value) {
return;
}
$join = new views_join();
$join->definition['left_table'] = $this->table_alias;
$join->definition['left_field'] = $this->real_field;
$join->definition['table'] = 'node';
$join->definition['field'] = 'tnid';
$join->definition['type'] = 'LEFT';
$join->construct();
$join->extra = array(array(
'field' => 'language',
'value' => $this->value,
));
$table_alias = $this->query->add_table('node', $this->relationship, $join);
$this->query->add_where_expression($this->options['group'], "{$this->table_alias}.language != :language", array(':language' => $this->value));
if ($this->target_status == 'untranslated_or_outdated') {
$this->query->add_where_expression($this->options['group'], "($table_alias.nid IS NULL OR {$this->table_alias}.translate = 1)");
}
elseif ($this->target_status == 'outdated') {
$this->query->add_where_expression($this->options['group'], "{$this->table_alias}.translate = 1");
}
elseif ($this->target_status == 'untranslated') {
$this->query->add_where_expression($this->options['group'], "$table_alias.nid IS NULL");
}
}
/**
* {@inheritdoc}
*/
function value_form(&$form, &$form_state) {
$options = array();
foreach (language_list() as $langcode => $language) {
$options[$langcode] = $language->name;
}
$identifier = $this->options['expose']['identifier'];
$form['value'][$identifier] = array(
'#type' => 'select',
'#options' => $options,
'#empty_option' => t('Any'),
'#id' => 'tmgmt_node_missing_target_language',
'#element_validate' => array('tmgmt_node_views_exposed_target_language_validate'),
);
// Attach css to style the target_status element inline.
$form['#attached']['css'][] = drupal_get_path('module', 'tmgmt_node_ui') . '/tmgmt_node_ui.source_overview.css';
$form['value']['target_status'] = array(
'#type' => 'select',
'#title' => t('Target status'),
'#options' => array(
'untranslated_or_outdated' => t('Untranslated or outdated'),
'untranslated' => t('Untranslated'),
'outdated' => t('Outdated'),
),
'#states' => array(
'invisible' => array(
':input[id="tmgmt_node_missing_target_language"]' => array('value' => ''),
),
),
);
}
/**
* {@inheritdoc}
*/
function accept_exposed_input($input) {
$return = parent::accept_exposed_input($input);
if ($return && isset($input['target_status'])) {
$this->target_status = $input['target_status'];
}
return $return;
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* @file
* Contains tmgmt_node_ui_handler_filter_node_translatable_types.
*/
/**
* Limits node types to those enabled for content translation.
*/
class tmgmt_node_ui_handler_filter_node_translatable_types extends views_handler_filter {
/**
* {@inheritdoc}
*/
function query() {
$this->ensure_my_table();
$valid_types = array_keys(tmgmt_source_translatable_item_types('node'));
if ($valid_types) {
$this->query->add_where($this->options['group'], "$this->table_alias.$this->real_field", array_values($valid_types), 'IN');
}
else {
// There are no valid translatable node types, do not return any results.
$this->query->add_where_expression($this->options['group'], '1 = 0');
}
}
/**
* {@inheritdoc}
*/
function admin_summary() { }
}

View File

@@ -0,0 +1,140 @@
<?php
/**
* @file
* Please supply a file description.
*/
class TMGMTNodeSourceViewsController extends TMGMTDefaultSourceViewsController {
/**
* {@inheritdoc}
*/
public function views_data() {
// Relationships between job items and nodes.
$data['tmgmt_job_item']['job_item_to_node'] = array(
'title' => t('Content'),
'help' => t('Content that is associated with this job item.'),
'real field' => 'item_id',
'relationship' => array(
'label' => t('Content'),
'base' => 'node',
'base field' => 'vid',
'relationship field' => 'item_id',
'extra' => array(
array(
'table' => 'tmgmt_job_item',
'field' => 'item_type',
'operator' => '=',
'value' => 'node',
),
array(
'table' => 'tmgmt_job_item',
'field' => 'plugin',
'operator' => '=',
'value' => 'node',
),
),
),
);
$data['node']['node_to_job_item'] = array(
'title' => t('Translation job item'),
'help' => t('Job items of this node.'),
'relationship' => array(
'real field' => 'vid',
'label' => t('Translation job item'),
'base' => 'tmgmt_job_item',
'base field' => 'item_id',
'extra' => array(
array(
'field' => 'item_type',
'operator' => '=',
'value' => 'node',
),
array(
'field' => 'plugin',
'operator' => '=',
'value' => 'node',
),
),
),
);
$data['node']['tmgmt_translatable_types_all'] = array(
'group' => t('Content translation'),
'title' => t('All translatable types'),
'help' => t('Enforces that only nodes from node types which are translatable are '),
'filter' => array(
'handler' => 'tmgmt_node_ui_handler_filter_node_translatable_types',
'real field' => 'type',
),
);
$data['node']['tmgmt_node_missing_translation'] = array(
'group' => t('Content translation'),
'title' => t('Missing translation'),
'help' => t('Enables the search for nodes with missing translation ofr the specified language'),
'filter' => array(
'handler' => 'tmgmt_node_handler_filter_missing_translation',
'real field' => 'nid',
),
);
$data['node']['tmgmt_jobs'] = array(
'title' => t('Translation jobs'),
'help' => t('Shows all translation jobs which contains this node'),
'field' => array(
'handler' => 'tmgmt_node_ui_handler_field_jobs',
'real field' => 'nid',
),
);
$data['node']['tmgmt_job_item'] = array(
'title' => t('Job item'),
'real field' => 'vid',
'relationship' => array(
'title' => t('Translation job item'),
'label' => t('Translation job item'),
'base' => 'tmgmt_job_item',
'base field' => 'item_id',
'extra' => array(
array(
'field' => 'item_type',
'operator' => '=',
'value' => 'node',
),
array(
'field' => 'plugin',
'operator' => '=',
'value' => 'node',
),
),
),
);
$data['node']['translation_language_status'] = array(
'group' => t('Content translation'),
'title' => t('All translation languages'),
'help' => t('Display all target lanuages.'),
'real field' => 'tnid',
'field' => array(
'handler' => 'tmgmt_node_handler_field_translation_language_status',
),
);
$data['node']['translation_language_status_single'] = array(
'title' => t('All translation languages (single)'),
'help' => t("Don't use this in the user interface."),
'field' => array(
'handler' => 'tmgmt_node_handler_field_translation_language_status_single',
),
);
$data['node']['tmgmt_translatable_types_select'] = array(
'group' => t('Content translation'),
'title' => t('Select translatable content types'),
'help' => t('Allows to filter on specific translatable types.'),
'filter' => array(
'handler' => 'views_handler_filter_in_operator',
'real field' => 'type',
'options callback' => 'tmgmt_source_translatable_item_types',
'options arguments' => array($this->pluginType),
),
);
return $data;
}
}

View File

@@ -0,0 +1,340 @@
<?php
$view = new view();
$view->name = 'tmgmt_node_source_overview';
$view->description = 'Node source overview for bulk operations.';
$view->tag = 'Translation Management';
$view->base_table = 'node';
$view->human_name = 'Node Source Overview';
$view->core = 7;
$view->api_version = '3.0';
$view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
/* Display: Master */
$handler = $view->new_display('default', 'Master', 'default');
$handler->display->display_options['title'] = 'Content overview';
$handler->display->display_options['use_more_always'] = FALSE;
$handler->display->display_options['group_by'] = TRUE;
$handler->display->display_options['access']['type'] = 'perm';
$handler->display->display_options['access']['perm'] = 'create translation jobs';
$handler->display->display_options['cache']['type'] = 'none';
$handler->display->display_options['query']['type'] = 'views_query';
$handler->display->display_options['query']['options']['query_comment'] = FALSE;
$handler->display->display_options['exposed_form']['type'] = 'basic';
$handler->display->display_options['exposed_form']['options']['submit_button'] = 'Search';
$handler->display->display_options['pager']['type'] = 'full';
$handler->display->display_options['pager']['options']['items_per_page'] = variable_get('tmgmt_source_list_limit', 20);
$handler->display->display_options['pager']['options']['offset'] = '0';
$handler->display->display_options['pager']['options']['id'] = '0';
$handler->display->display_options['pager']['options']['quantity'] = '9';
$handler->display->display_options['style_plugin'] = 'table';
$handler->display->display_options['style_options']['columns'] = array(
'title' => 'title',
);
$handler->display->display_options['style_options']['default'] = '-1';
$handler->display->display_options['style_options']['info'] = array(
'title' => array(
'sortable' => 0,
'default_sort_order' => 'asc',
'align' => '',
'separator' => '',
'empty_column' => 0,
),
);
/* No results behavior: Global: Text area */
$handler->display->display_options['empty']['area']['id'] = 'area';
$handler->display->display_options['empty']['area']['table'] = 'views';
$handler->display->display_options['empty']['area']['field'] = 'area';
$handler->display->display_options['empty']['area']['content'] = 'There are no nodes that match the specified filter criteria.';
$handler->display->display_options['empty']['area']['format'] = 'filtered_html';
/* Relationship: User */
$handler->display->display_options['relationships']['uid']['id'] = 'uid';
$handler->display->display_options['relationships']['uid']['table'] = 'node';
$handler->display->display_options['relationships']['uid']['field'] = 'uid';
$handler->display->display_options['relationships']['uid']['ui_name'] = 'User';
$handler->display->display_options['relationships']['uid']['label'] = 'User';
/* Field: Bulk operations */
$handler->display->display_options['fields']['views_bulk_operations']['id'] = 'views_bulk_operations';
$handler->display->display_options['fields']['views_bulk_operations']['table'] = 'node';
$handler->display->display_options['fields']['views_bulk_operations']['field'] = 'views_bulk_operations';
$handler->display->display_options['fields']['views_bulk_operations']['ui_name'] = 'Bulk operations';
$handler->display->display_options['fields']['views_bulk_operations']['label'] = '<!--views-bulk-operations-select-all-->';
$handler->display->display_options['fields']['views_bulk_operations']['element_label_colon'] = FALSE;
$handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['display_type'] = '1';
$handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['enable_select_all_pages'] = 1;
$handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['force_single'] = 0;
$handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['entity_load_capacity'] = '10';
$handler->display->display_options['fields']['views_bulk_operations']['vbo_operations'] = array(
'rules_component::tmgmt_node_ui_tmgmt_nodes_add_items_to_cart' => array(
'selected' => 1,
'skip_confirmation' => 1,
'override_label' => 0,
'label' => '',
),
'action::node_assign_owner_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::views_bulk_operations_delete_item' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::views_bulk_operations_script_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::node_make_sticky_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::node_make_unsticky_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::views_bulk_operations_modify_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
'settings' => array(
'show_all_tokens' => 1,
'display_values' => array(
'_all_' => '_all_',
),
),
),
'action::views_bulk_operations_argument_selector_action' => array(
'selected' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
'settings' => array(
'url' => '',
),
),
'action::node_promote_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::node_publish_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::node_unpromote_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'rules_component::tmgmt_node_ui_request_translation' => array(
'selected' => 0,
'skip_confirmation' => 1,
'override_label' => 0,
'label' => '',
),
'action::tmgmt_node_ui_checkout_multiple_action' => array(
'selected' => 1,
'skip_confirmation' => 1,
'override_label' => 0,
'label' => '',
),
'action::node_save_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::system_send_email_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::node_unpublish_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::node_unpublish_by_keyword_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
);
/* Field: Title */
$handler->display->display_options['fields']['title']['id'] = 'title';
$handler->display->display_options['fields']['title']['table'] = 'node';
$handler->display->display_options['fields']['title']['field'] = 'title';
$handler->display->display_options['fields']['title']['ui_name'] = 'Title';
$handler->display->display_options['fields']['title']['label'] = 'Title (in source language)';
$handler->display->display_options['fields']['title']['alter']['word_boundary'] = FALSE;
$handler->display->display_options['fields']['title']['alter']['ellipsis'] = FALSE;
/* Field: Type */
$handler->display->display_options['fields']['type']['id'] = 'type';
$handler->display->display_options['fields']['type']['table'] = 'node';
$handler->display->display_options['fields']['type']['field'] = 'type';
$handler->display->display_options['fields']['type']['ui_name'] = 'Type';
/* Field: All translation languages */
$handler->display->display_options['fields']['translation_language_status_1']['id'] = 'translation_language_status_1';
$handler->display->display_options['fields']['translation_language_status_1']['table'] = 'node';
$handler->display->display_options['fields']['translation_language_status_1']['field'] = 'translation_language_status';
$handler->display->display_options['fields']['translation_language_status_1']['ui_name'] = 'All translation languages';
$handler->display->display_options['fields']['translation_language_status_1']['exclude'] = TRUE;
/* Field: Author */
$handler->display->display_options['fields']['name']['id'] = 'name';
$handler->display->display_options['fields']['name']['table'] = 'users';
$handler->display->display_options['fields']['name']['field'] = 'name';
$handler->display->display_options['fields']['name']['relationship'] = 'uid';
$handler->display->display_options['fields']['name']['ui_name'] = 'Author';
$handler->display->display_options['fields']['name']['label'] = 'Author';
/* Field: Updated date */
$handler->display->display_options['fields']['changed']['id'] = 'changed';
$handler->display->display_options['fields']['changed']['table'] = 'node';
$handler->display->display_options['fields']['changed']['field'] = 'changed';
$handler->display->display_options['fields']['changed']['ui_name'] = 'Updated date';
$handler->display->display_options['fields']['changed']['date_format'] = 'short';
/* Sort criterion: Post date */
$handler->display->display_options['sorts']['created']['id'] = 'created';
$handler->display->display_options['sorts']['created']['table'] = 'node';
$handler->display->display_options['sorts']['created']['field'] = 'created';
$handler->display->display_options['sorts']['created']['ui_name'] = 'Post date';
$handler->display->display_options['sorts']['created']['order'] = 'DESC';
/* Filter criterion: Content: Title */
$handler->display->display_options['filters']['title']['id'] = 'title';
$handler->display->display_options['filters']['title']['table'] = 'node';
$handler->display->display_options['filters']['title']['field'] = 'title';
$handler->display->display_options['filters']['title']['operator'] = 'word';
$handler->display->display_options['filters']['title']['group'] = 1;
$handler->display->display_options['filters']['title']['exposed'] = TRUE;
$handler->display->display_options['filters']['title']['expose']['operator_id'] = 'title_op';
$handler->display->display_options['filters']['title']['expose']['label'] = 'Node title';
$handler->display->display_options['filters']['title']['expose']['operator'] = 'title_op';
$handler->display->display_options['filters']['title']['expose']['identifier'] = 'title';
/* Filter criterion: Published */
$handler->display->display_options['filters']['status']['id'] = 'status';
$handler->display->display_options['filters']['status']['table'] = 'node';
$handler->display->display_options['filters']['status']['field'] = 'status';
$handler->display->display_options['filters']['status']['ui_name'] = 'Published';
$handler->display->display_options['filters']['status']['value'] = '1';
$handler->display->display_options['filters']['status']['group'] = 1;
$handler->display->display_options['filters']['status']['exposed'] = TRUE;
$handler->display->display_options['filters']['status']['expose']['operator_id'] = '';
$handler->display->display_options['filters']['status']['expose']['label'] = 'Published';
$handler->display->display_options['filters']['status']['expose']['operator'] = 'status_op';
$handler->display->display_options['filters']['status']['expose']['identifier'] = 'status';
$handler->display->display_options['filters']['status']['expose']['required'] = TRUE;
/* Filter criterion: Source translation */
$handler->display->display_options['filters']['source_translation']['id'] = 'source_translation';
$handler->display->display_options['filters']['source_translation']['table'] = 'node';
$handler->display->display_options['filters']['source_translation']['field'] = 'source_translation';
$handler->display->display_options['filters']['source_translation']['ui_name'] = 'Source translation';
$handler->display->display_options['filters']['source_translation']['operator'] = '1';
$handler->display->display_options['filters']['source_translation']['group'] = 1;
/* Filter criterion: Content: Language */
$handler->display->display_options['filters']['language']['id'] = 'language';
$handler->display->display_options['filters']['language']['table'] = 'node';
$handler->display->display_options['filters']['language']['field'] = 'language';
$handler->display->display_options['filters']['language']['operator'] = 'not in';
$handler->display->display_options['filters']['language']['value'] = array(
'und' => 'und',
);
$handler->display->display_options['filters']['language']['group'] = 1;
/* Filter criterion: Content: Language */
$handler->display->display_options['filters']['language_1']['id'] = 'language_1';
$handler->display->display_options['filters']['language_1']['table'] = 'node';
$handler->display->display_options['filters']['language_1']['field'] = 'language';
$handler->display->display_options['filters']['language_1']['group'] = 1;
$handler->display->display_options['filters']['language_1']['exposed'] = TRUE;
$handler->display->display_options['filters']['language_1']['expose']['operator_id'] = 'language_1_op';
$handler->display->display_options['filters']['language_1']['expose']['label'] = 'Source language';
$handler->display->display_options['filters']['language_1']['expose']['operator'] = 'language_1_op';
$handler->display->display_options['filters']['language_1']['expose']['identifier'] = 'language_1';
/* Filter criterion: Content translation: Select translatable content types */
$handler->display->display_options['filters']['tmgmt_translatable_types_select']['id'] = 'tmgmt_translatable_types_select';
$handler->display->display_options['filters']['tmgmt_translatable_types_select']['table'] = 'node';
$handler->display->display_options['filters']['tmgmt_translatable_types_select']['field'] = 'tmgmt_translatable_types_select';
$handler->display->display_options['filters']['tmgmt_translatable_types_select']['exposed'] = TRUE;
$handler->display->display_options['filters']['tmgmt_translatable_types_select']['expose']['operator_id'] = 'tmgmt_translatable_types_select_op';
$handler->display->display_options['filters']['tmgmt_translatable_types_select']['expose']['label'] = 'Content type';
$handler->display->display_options['filters']['tmgmt_translatable_types_select']['expose']['operator'] = 'tmgmt_translatable_types_select_op';
$handler->display->display_options['filters']['tmgmt_translatable_types_select']['expose']['identifier'] = 'tmgmt_translatable_types_select';
/* Filter criterion: Content translation: All translatable types */
$handler->display->display_options['filters']['tmgmt_translatable_types_all']['id'] = 'tmgmt_translatable_types_all';
$handler->display->display_options['filters']['tmgmt_translatable_types_all']['table'] = 'node';
$handler->display->display_options['filters']['tmgmt_translatable_types_all']['field'] = 'tmgmt_translatable_types_all';
/* Filter criterion: Content translation: Missing translation */
$handler->display->display_options['filters']['tmgmt_node_missing_translation']['id'] = 'tmgmt_node_missing_translation';
$handler->display->display_options['filters']['tmgmt_node_missing_translation']['table'] = 'node';
$handler->display->display_options['filters']['tmgmt_node_missing_translation']['field'] = 'tmgmt_node_missing_translation';
$handler->display->display_options['filters']['tmgmt_node_missing_translation']['exposed'] = TRUE;
$handler->display->display_options['filters']['tmgmt_node_missing_translation']['expose']['operator_id'] = 'tmgmt_node_missing_translation_op';
$handler->display->display_options['filters']['tmgmt_node_missing_translation']['expose']['label'] = 'Target language';
$handler->display->display_options['filters']['tmgmt_node_missing_translation']['expose']['operator'] = 'tmgmt_node_missing_translation_op';
$handler->display->display_options['filters']['tmgmt_node_missing_translation']['expose']['identifier'] = 'tmgmt_node_missing_translation';
/* Display: Page */
$handler = $view->new_display('page', 'Page', 'page');
$handler->display->display_options['path'] = 'admin/tmgmt/sources/node';
$handler->display->display_options['menu']['type'] = 'tab';
$handler->display->display_options['menu']['title'] = 'Content';
$handler->display->display_options['menu']['weight'] = -20;
$handler->display->display_options['menu']['context'] = 0;
$translatables['tmgmt_node_source_overview'] = array(
t('Master'),
t('Content overview'),
t('more'),
t('Search'),
t('Reset'),
t('Sort by'),
t('Asc'),
t('Desc'),
t('Items per page'),
t('- All -'),
t('Offset'),
t('« first'),
t(' previous'),
t('next '),
t('last »'),
t('There are no nodes that match the specified filter criteria.'),
t('User'),
t('<!--views-bulk-operations-select-all-->'),
t('Title (in source language)'),
t('Type'),
t('All translation languages'),
t('Author'),
t('Updated date'),
t('Node title'),
t('Published'),
t('Source language'),
t('Content type'),
t('Page'),
);