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,20 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xml:lang="de-CH" lang="de-CH" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta name="JobID" content="<?php echo $tjid; ?>" />
<meta name="languageSource" content="<?php echo $source_language; ?>" />
<meta name="languageTarget" content="<?php echo $target_language; ?>" />
<title>Job ID <?php echo $tjid; ?></title>
</head>
<body>
<?php foreach ($items as $item_key => $item): ?>
<div class="asset" id="<?php echo $item_key; ?>">
<?php foreach ($item as $field_key => $field): ?>
<div class="atom" id="<?php echo $field_key; ?>"><?php echo $field['#text']; ?></div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
</body>
</html>

View File

@@ -0,0 +1,58 @@
<?php
/*
* @file
* API and hook documentation for the File Translator module.
*/
/**
* Provide information about available file format to export to and import from.
*
* @return
* An array of available file format plugin definitions. The key is the file
* extension for that format. It is therefore currently not possible to have
* two file formats which share the same file extension as there needs to be
* a way to identify them for the import. Each plugin info array then consists
* of a label and a plugin controller class, which needs to implement
* TMGMTFileFormatInterface.
*
* @see hook_tmgmt_file_format_plugin_info_alter()
*/
function hook_tmgmt_file_format_plugin_info() {
return array(
'xlf' => array(
'label' => t('XLIFF'),
'plugin controller class' => 'TMGMTFileFormatXLIFF',
),
'html' => array(
'label' => t('HTML'),
'plugin controller class' => 'TMGMTFileFormatHTML',
),
);
}
/**
* Provide information about available text processors.
*
* @return array
* An array of available text processor definitions. The key is the text
* processor name.
*/
function hook_tmgmt_file_text_processor_plugin_info() {
return array(
'mask_html_for_xliff' => array(
'label' => t('Escape HTML'),
'processor class' => 'TMGMTFileXLIFFMaskHTMLProcessor',
),
);
}
/**
* Alter file format plugins provided by other modules.
*
* @see hook_tmgmt_file_format_plugin_info()
*/
function hook_tmgmt_file_format_plugin_info_alter($file_formats) {
// Switch the used HTML plugin controller class.
$file_formats['html']['plugin controller class'] = 'MyModuleCustomizedHTML';
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* @file
* Drush integration for tmgmt_file.
*/
/**
* Implements hook_drush_command().
*/
function tmgmt_file_drush_command() {
$items = array();
$items['tmgmt_translate_import'] = array(
'description' => 'Import XLIFF translation files',
'arguments' => array(
'name' => 'Directory path that is search for *.xlf files or a file name',
),
'aliases' => array('tmti'),
);
return $items;
}
/**
* Import XLIFF files from a directory or single file.
*/
function drush_tmgmt_file_tmgmt_translate_import($name = NULL) {
if (!$name) {
return drush_set_error(dt('You need to provide a directory path or filename.'));
}
if (!file_exists($name)) {
// Drush changes the current working directory to the drupal root directory.
// Also check the current directory.
if (!file_exists(drush_cwd() . '/' . $name)) {
return drush_set_error(dt('@name does not exists or is not accessible.', array('@name' => $name)));
}
else {
// The path is relative to the current directory, update the variable.
$name = drush_cwd() . '/' . $name;
}
}
if (is_dir($name)) {
drush_log(dt('Scanning dir @dir.', array('@dir' => $name)), 'success');
$files = file_scan_directory($name, '/.*\.xlf$/');
if (empty($files)) {
drush_set_error(dt('No files found to import in @name.', array('@name' => $name)));
}
}
else {
// Create the structure expected by the loop below.
$files = array($name => (object)array('name' => basename($name)));
}
$controller = tmgmt_file_format_controller('xlf');
foreach ($files as $path => $info) {
$job = $controller->validateImport($path);
if (empty($job)) {
drush_log(dt('No translation job found for @filename.', array('@filename' => $info->name)), 'error');
continue;
}
if ($job->isFinished()) {
drush_log(dt('Skipping @filename for finished job @name (#@id).', array('@filename' => $info->name, '@name' => $job->label(), '@id' => $job->tjid)), 'warning');
continue;
}
try {
// Validation successful, start import.
$job->addTranslatedData($controller->import($path));
drush_log(dt('Successfully imported file @filename for translation job @name (#@id).', array('@filename' => $info->name, '@name' => $job->label(), '@id' => $job->tjid)), 'success');
}
catch (Exception $e) {
drush_log(dt('Failed importing file @filename: @error', array('@filename' => $info->name, '@error' => $e->getMessage())), 'error');
}
}
}
?>

View File

@@ -0,0 +1,103 @@
<?php
/**
* Export into HTML.
*/
class TMGMTFileFormatHTML implements TMGMTFileFormatInterface {
/**
* Returns base64 encoded data that is safe for use in xml ids.
*/
protected function encodeIdSafeBase64($data) {
// Prefix with a b to enforce that the first character is a letter.
return 'b' . rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
/**
* Returns decoded id safe base64 data.
*/
protected function decodeIdSafeBase64($data) {
// Remove prefixed b.
$data = substr($data, 1);
return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT));
}
/**
* {@inheritdoc}
*/
public function export(TMGMTJob $job, $conditions = array()) {
$items = array();
foreach ($job->getItems($conditions) as $item) {
$data = array_filter(tmgmt_flatten_data($item->getData()), '_tmgmt_filter_data');
foreach ($data as $key => $value) {
$items[$item->tjiid][$this->encodeIdSafeBase64($item->tjiid . '][' . $key)] = $value;
}
}
return theme('tmgmt_file_html_template', array(
'tjid' => $job->tjid,
'source_language' => $job->getTranslator()->mapToRemoteLanguage($job->source_language),
'target_language' => $job->getTranslator()->mapToRemoteLanguage($job->target_language),
'items' => $items,
));
}
/**
* {@inheritdoc}
*/
public function import($imported_file, $is_file = TRUE) {
$dom = new DOMDocument();
$dom->loadHTMLFile($imported_file);
$xml = simplexml_import_dom($dom);
$data = array();
foreach ($xml->xpath("//div[@class='atom']") as $atom) {
// Assets are our strings (eq fields in nodes).
$key = $this->decodeIdSafeBase64((string) $atom['id']);
$data[$key]['#text'] = (string) $atom;
}
return tmgmt_unflatten_data($data);
}
/**
* {@inheritdoc}
*/
public function validateImport($imported_file) {
$dom = new DOMDocument();
if (!$dom->loadHTMLFile($imported_file)) {
return FALSE;
}
$xml = simplexml_import_dom($dom);
// Collect meta information.
$meta_tags = $xml->xpath('//meta');
$meta = array();
foreach ($meta_tags as $meta_tag) {
$meta[(string) $meta_tag['name']] = (string) $meta_tag['content'];
}
// Check required meta tags.
foreach (array('JobID', 'languageSource', 'languageTarget') as $name) {
if (!isset($meta[$name])) {
return FALSE;
}
}
// Attempt to load the job.
if (!$job = tmgmt_job_load($meta['JobID'])) {
drupal_set_message(t('The imported file job id @file_tjid is not available.', array(
'@file_tjid' => $job->tjid,
)), 'error');
return FALSE;
}
// Check language.
if ($meta['languageSource'] != $job->getTranslator()->mapToRemoteLanguage($job->source_language) ||
$meta['languageTarget'] != $job->getTranslator()->mapToRemoteLanguage($job->target_language)) {
return FALSE;
}
// Validation successful.
return $job;
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* Interface for exporting to a given file format.
*/
interface TMGMTFileFormatInterface {
/**
* Return the file content for the job data.
*
* @param $job
* The translation job object to be exported.
* @param array $conditions
* (optional) An array containing list of conditions.
*
* @return
* String with the file content.
*/
function export(TMGMTJob $job, $conditions = array());
/**
* Validates that the given file is valid and can be imported.
*
* @todo this function should NOT return a job. We need a import processor
* instance instead to deal with the import context.
*
* @param string $imported_file
* File path to the file to be imported.
*
* @return TMGMTJob
* Returns the corresponding translation job entity if the import file is
* valid, FALSE otherwise.
*/
public function validateImport($imported_file);
/**
* Converts an exported file content back to the translated data.
*
* @param string $imported_file
* Path to a file or an XML string to import.
* @param bool $is_file
* (optional) Whether $imported_file is the path to a file or not.
*
* @return
* Translated data array.
*/
function import($imported_file, $is_file = TRUE);
}

View File

@@ -0,0 +1,635 @@
<?php
/**
* Export to XLIFF format.
*
* The XLIFF processor follows this specification:
* @link http://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2-cd02.html
*
* The purpose of this class is to mask or process HTML elements in the source
* and target elements so that translation tools are able to understand which
* content needs to be translated and ignored.
*
* On the other hand we need to properly unmask the XLIFF markup back to HTML on
* the translation import. So the process is bidirectional and prior to running
* the unmasking process we try to validate the integrity in the
* validateJobTranslationUponImport() method. Currently the integrity check
* involves only a counter of XLIFF elements that have been created during
* source processing and has to mach number of XLIFF elements being imported
* with the translation.
*
* To process the content DOMDocument object is used due to its ability to
* read broken HTML. This also implies that if broken HTML is in the source
* content the translation content will be fixed into the extend of DOMDocument
* abilities.
*
* Following is implemented:
* - All pair tags get escaped using <bpt><ept> markup.
* - <br> tags are marked with <x ctype="lb">.
* - <img> tags are marked with <ph ctype="image"> tags. The title and alt
* attributes should have been extracted into <sub> elements, however are not
* as Trados studio triggers a fatal error in case there are two <sub>
* elements at the same level.
*
* Not implemented:
* - Attributes of <img> element are written only as attributes of <ph> element
* instead of using x-html: prefix. This results in conflict with own <ph>
* element's attributes such as "id". The reason why x-html prefix has not
* been used is that Trados studio triggered fatal error on xml validation.
* - Translatable attributes like title and alt.
* @link http://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2-cd02.html#elem_img
* - Forms - this is big part
* @link http://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2-cd02.html#HTMLForms
* - <pre> elements
* @link http://docs.oasis-open.org/xliff/v1.2/xliff-profile-html/xliff-profile-html-1.2-cd02.html#Elem_preformatted
*/
class TMGMTFileformatXLIFF extends XMLWriter implements TMGMTFileFormatInterface {
/**
* Contains a reference to the currently being exported job.
*
* @var TMGMTJob
*/
protected $job;
protected $importedXML;
protected $importedTransUnits;
/**
* Adds a job item to the xml export.
*
* @param $item
* The job item entity.
*/
protected function addItem(TMGMTJobItem $item) {
$this->startElement('group');
$this->writeAttribute('id', $item->tjiid);
// Add a note for the source label.
$this->writeElement('note', $item->getSourceLabel());
// @todo: Write in nested groups instead of flattening it.
$data = array_filter(tmgmt_flatten_data($item->getData()), '_tmgmt_filter_data');
foreach ($data as $key => $element) {
$this->addTransUnit($item->tjiid . '][' . $key, $element, $this->job);
}
$this->endElement();
}
/**
* Adds a single translation unit for a data element.
*
* @param $key
* The unique identifier for this data element.
* @param $element
* Array with the properties #text and optionally #label.
* @param TMGMTJob $job
* Translation job.
*/
protected function addTransUnit($key, $element, TMGMTJob $job) {
$key_array = tmgmt_ensure_keys_array($key);
$this->startElement('trans-unit');
$this->writeAttribute('id', $key);
$this->writeAttribute('resname', $key);
$this->startElement('source');
$this->writeAttribute('xml:lang', $this->job->getTranslator()->mapToRemoteLanguage($this->job->source_language));
if ($job->getSetting('xliff_cdata')) {
$this->writeCdata(trim($element['#text']));
}
elseif ($job->getSetting('xliff_processing')) {
$this->writeRaw($this->processForExport($element['#text'], $key_array));
}
else {
$this->text($element['#text']);
}
$this->endElement();
$this->startElement('target');
$this->writeAttribute('xml:lang', $this->job->getTranslator()->mapToRemoteLanguage($this->job->target_language));
if (!empty($element['#translation']['#text'])) {
if ($job->getSetting('xliff_processing')) {
$this->writeRaw($this->processForExport($element['#translation']['#text'], $key_array));
}
else {
$this->text($element['#translation']['#text']);
}
}
$this->endElement();
if (isset($element['#label'])) {
$this->writeElement('note', $element['#label']);
}
$this->endElement();
}
/**
* {@inheritdoc}
*/
public function export(TMGMTJob $job, $conditions = array()) {
$this->job = $job;
$this->openMemory();
$this->setIndent(true);
$this->setIndentString(' ');
$this->startDocument('1.0', 'UTF-8');
// Root element with schema definition.
$this->startElement('xliff');
$this->writeAttribute('version', '1.2');
$this->writeAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:1.2');
$this->writeAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
$this->writeAttribute('xsi:schemaLocation', 'urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-strict.xsd');
// File element.
$this->startElement('file');
$this->writeAttribute('original', 'xliff-core-1.2-strict.xsd');
$this->writeAttribute('source-language', $job->getTranslator()->mapToRemoteLanguage($job->source_language));
$this->writeAttribute('target-language', $job->getTranslator()->mapToRemoteLanguage($job->target_language));
$this->writeAttribute('datatype', 'plaintext');
// Date needs to be in ISO-8601 UTC
$this->writeAttribute('date', date('Y-m-d\Th:m:i\Z'));
$this->startElement('header');
$this->startElement('phase-group');
$this->startElement('phase');
$this->writeAttribute('tool-id', 'tmgmt');
$this->writeAttribute('phase-name', 'extraction');
$this->writeAttribute('process-name', 'extraction');
$this->writeAttribute('job-id', $job->tjid);
$this->endElement();
$this->endElement();
$this->startElement('tool');
$this->writeAttribute('tool-id', 'tmgmt');
$this->writeAttribute('tool-name', 'Drupal Translation Management Tools');
$this->endElement();
$this->endElement();
$this->startElement('body');
foreach ($job->getItems($conditions) as $item) {
$this->addItem($item);
}
// End the body, file and xliff tags.
$this->endElement();
$this->endElement();
$this->endElement();
$this->endDocument();
return $this->outputMemory();
}
/**
* {@inheritdoc}
*/
public function import($imported_file, $is_file = TRUE) {
if (!$this->getImportedXML($imported_file, $is_file)) {
return FALSE;
}
$phase = $this->importedXML->xpath("//xliff:phase[@phase-name='extraction']");
$phase = reset($phase);
$job = tmgmt_job_load((string) $phase['job-id']);
return tmgmt_unflatten_data($this->getImportedTargets($job));
}
/**
* {@inheritdoc}
*/
public function validateImport($imported_file) {
// Validates imported XLIFF file.
// Checks:
// - Job ID
// - Target ans source languages
// - Content integrity.
if (!($xml = $this->getImportedXML($imported_file))) {
drupal_set_message(t('The imported file is not a valid XML.'), 'error');
return FALSE;
}
// Check if our phase information is there.
$phase = $xml->xpath("//xliff:phase[@phase-name='extraction']");
if ($phase) {
$phase = reset($phase);
}
else {
drupal_set_message(t('The imported file is missing required XLIFF phase information.'), 'error');
return FALSE;
}
// Check if the job has a valid job reference.
if (!isset($phase['job-id'])) {
drupal_set_message(t('The imported file does not contain a job reference.'), 'error');
return FALSE;
}
// Attempt to load the job if none passed.
$job = tmgmt_job_load((int) $phase['job-id']);
if (empty($job)) {
drupal_set_message(t('The imported file job id @file_tjid is not available.', array(
'@file_tjid' => $phase['job-id'],
)), 'error');
return FALSE;
}
// @todo We use the $job to addMessage in case of failure. However the job
// context is not safe at this point.
// Compare source language.
if (!isset($xml->file['source-language']) || $job->getTranslator()->mapToRemoteLanguage($job->source_language) != $xml->file['source-language']) {
$job->addMessage('The imported file source language @file_language does not match the job source language @job_language.', array(
'@file_language' => empty($xml->file['source-language']) ? t('none') : $xml->file['source-language'],
'@job_language' => $job->source_language,
), 'error');
return FALSE;
}
// Compare target language.
if (!isset($xml->file['target-language']) || $job->getTranslator()->mapToRemoteLanguage($job->target_language) != $xml->file['target-language']) {
$job->addMessage('The imported file target language @file_language does not match the job target language @job_language.', array(
'@file_language' => empty($xml->file['target-language']) ? t('none') : $xml->file['target-language'],
'@job_language' => $job->target_language,
), 'error');
return FALSE;
}
$targets = $this->getImportedTargets($job);
if (empty($targets)) {
$job->addMessage('The imported file seems to be missing translation.', 'error');
return FALSE;
}
// In case we do not do xliff processing we cannot do the elements
// count validation.
if (!$job->getSetting('xliff_processing')) {
return $job;
}
$reader = new XMLReader();
$xliff_validation = $job->getSetting('xliff_validation');
foreach ($targets as $id => $target) {
$array_key = tmgmt_ensure_keys_array($id);
$job_item = tmgmt_job_item_load(array_shift($array_key));
$count = 0;
$reader->XML('<translation>' . $target['#text'] . '</translation>');
while ($reader->read()) {
if (in_array($reader->name, array('translation', '#text'))) {
continue;
}
$count++;
}
if (!isset($xliff_validation[$id]) || $xliff_validation[$id] != $count) {
$job_item->addMessage('Failed to validate semantic integrity of %key element. Please check also the HTML code of the element in the review process.',
array('%key' => tmgmt_ensure_keys_string($array_key)));
}
}
// Validation successful.
return $job;
}
/**
* Returns the simple XMLElement object.
*
* @param string $imported_file
* Path to a file or an XML string to import.
* @param bool $is_file
* (optional) Whether $imported_file is the path to a file or not.
*
* @return bool|\SimpleXMLElement
* The parsed SimpleXMLElement object. FALSE in case of failed parsing.
*/
protected function getImportedXML($imported_file, $is_file = TRUE) {
if (empty($this->importedXML)) {
// It is not possible to load the file directly with simplexml as it gets
// url encoded due to the temporary://. This is a PHP bug, see
// https://bugs.php.net/bug.php?id=61469
if ($is_file) {
$imported_file = file_get_contents($imported_file);
}
if (!($this->importedXML = simplexml_load_string($imported_file))) {
return FALSE;
}
// Register the XLIFF namespace, required for xpath.
$this->importedXML->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:1.2');
}
return $this->importedXML;
}
protected function getImportedTargets(TMGMTJob $job) {
if (empty($this->importedXML)) {
return FALSE;
}
if (empty($this->importedTransUnits)) {
$reader = new XMLReader();
foreach ($this->importedXML->xpath('//xliff:trans-unit') as $unit) {
if (!$job->getSetting('xliff_processing')) {
$this->importedTransUnits[(string) $unit['id']]['#text'] = (string) $unit->target;
continue;
}
$reader->XML($unit->target->asXML());
$reader->read();
$this->importedTransUnits[(string) $unit['id']]['#text'] =
$this->processForImport($reader->readInnerXML(), $job);
}
}
return $this->importedTransUnits;
}
/**
* Processes trans-unit/target to rebuild back the HTML.
*
* @param string $translation
* Job data array.
* @param TMGMTJob $job
* Translation job.
*
* @return string
*/
protected function processForImport($translation, TMGMTJob $job) {
// In case we do not want to do xliff processing return the translation as
// is.
if (!$job->getSetting('xliff_processing')) {
return $translation;
}
$reader = new XMLReader();
$reader->XML('<translation>' . $translation . '</translation>');
$text = '';
while ($reader->read()) {
// If the current element is text append it to the result text.
if ($reader->name == '#text' || $reader->name == '#cdata-section') {
$text .= $reader->value;
}
elseif ($reader->name == 'x') {
if ($reader->getAttribute('ctype') == 'lb') {
$text .= '<br />';
}
}
elseif ($reader->name == 'ph') {
if ($reader->getAttribute('ctype') == 'image') {
$text .= '<img';
while ($reader->moveToNextAttribute()) {
// @todo - we have to use x-html: prefixes for attributes.
if ($reader->name != 'ctype' && $reader->name != 'id') {
$text .= " {$reader->name}=\"{$reader->value}\"";
}
}
$text .= ' />';
}
}
}
return $text;
}
/**
* Helper function to process the source text.
*
* @param string $source
* Job data array.
* @param array $key_array
* The source item data key.
*
* @return string
*/
protected function processForExport($source, array $key_array) {
$tjiid = $key_array[0];
$key_string = tmgmt_ensure_keys_string($key_array);
// The reason why we use DOMDocument object here and not just XMLReader
// is the DOMDocument's ability to deal with broken HTML.
$dom = new DOMDocument();
// We need to append the head with encoding so that special characters
// are read correctly.
$dom->loadHTML("<html><head><meta http-equiv='Content-type' content='text/html; charset=UTF-8' /></head><body>" . $source . '</body></html>');
$iterator = new RecursiveIteratorIterator(
new RecursiveDOMIterator($dom),
RecursiveIteratorIterator::SELF_FIRST);
$writer = new XMLWriter();
$writer->openMemory();
$writer->startDocument('1.0', 'UTF-8');
$writer->startElement('wrapper');
$tray = array();
$non_pair_tags = array('br', 'img');
if (!isset($this->job->settings['xliff_validation'])) {
$this->job->settings['xliff_validation'] = array();
}
$xliff_validation = $this->job->settings['xliff_validation'];
/** @var DOMElement $node */
foreach ($iterator as $node) {
if (in_array($node->nodeName, array('html', 'body', 'head', 'meta'))) {
continue;
}
if ($node->nodeType === XML_ELEMENT_NODE) {
// Increment the elements count and compose element id.
if (!isset($xliff_validation[$key_string])) {
$xliff_validation[$key_string] = 0;
}
$xliff_validation[$key_string]++;
$id = 'tjiid' . $tjiid . '-' . $xliff_validation[$key_string];
$is_pair_tag = !in_array($node->nodeName, $non_pair_tags);
if ($is_pair_tag) {
$this->writeBPT($writer, $node, $id);
}
elseif ($node->nodeName == 'img') {
$this->writeIMG($writer, $node, $id);
}
elseif ($node->nodeName == 'br') {
$this->writeBR($writer, $node, $id);
}
// Add to tray new element info.
$tray[$id] = array(
'name' => $node->nodeName,
'id' => $id,
'value' => $node->nodeValue,
'built_text' => '',
'is_pair_tag' => $is_pair_tag,
);
}
// The current node is a text.
elseif ($node->nodeName == '#text') {
// Add the node value to the text output.
$writer->writeCdata($this->toEntities($node->nodeValue));
foreach ($tray as &$info) {
$info['built_text'] .= $node->nodeValue;
}
}
// Reverse so that pair tags are closed in the expected order.
$reversed_tray = array_reverse($tray);
foreach ($reversed_tray as $_info) {
// If the build_text equals to the node value and it is not a pair tag
// add the end pair tag markup.
if ($_info['value'] == $_info['built_text'] && $_info['is_pair_tag']) {
// Count also for the closing elements.
$xliff_validation[$key_string]++;
$this->writeEPT($writer, $_info['name'], $_info['id']);
// When the end pair tag has been written unset the element info
// from the tray.
unset($tray[$_info['id']]);
}
}
}
// Set the xliff_validation data and save the job.
$this->job->settings['xliff_validation'] = $xliff_validation;
$this->job->save();
$writer->endElement();
// Load the output with XMLReader so that we can easily get the inner xml.
$reader = new XMLReader();
$reader->XML($writer->outputMemory());
$reader->read();
return $reader->readInnerXML();
}
/**
* Writes br tag.
*
* @param XMLWriter $writer
* Writer that writes the output.
* @param DOMElement $node
* Current node.
* @param $id
* Current node id.
*/
protected function writeBR(XMLWriter $writer, DOMElement $node, $id) {
$writer->startElement('x');
$writer->writeAttribute('id', $id);
$writer->writeAttribute('ctype', 'lb');
$writer->endElement();
}
/**
* Writes beginning pair tag.
*
* @param XMLWriter $writer
* Writer that writes the output.
* @param DOMElement $node
* Current node.
* @param $id
* Current node id.
*/
protected function writeBPT(XMLWriter $writer, DOMElement $node, $id) {
$beginning_tag = '<' . $node->nodeName;
if ($node->hasAttributes()) {
$attributes = array();
/** @var DOMAttr $attribute */
foreach ($node->attributes as $attribute) {
$attributes[] = $attribute->name . '="' . $attribute->value . '"';
}
$beginning_tag .= ' '. implode(' ', $attributes);
}
$beginning_tag .= '>';
$writer->startElement('bpt');
$writer->writeAttribute('id', $id);
$writer->text($beginning_tag);
$writer->endElement();
}
/**
* Writes ending pair tag.
*
* @param XMLWriter $writer
* Writer that writes the output.
* @param string $name
* Ending tag name.
* @param $id
* Current node id.
*/
protected function writeEPT(XMLWriter $writer, $name, $id) {
$writer->startElement('ept');
$writer->writeAttribute('id', $id);
$writer->text('</' . $name . '>');
$writer->endElement();
}
/**
* Writes img tag.
*
* Note that alt and title attributes are not written as sub elements as
* Trados studio is not able to deal with two sub elements at one level.
*
* @param XMLWriter $writer
* Writer that writes the output.
* @param DOMElement $node
* Current node.
* @param $id
* Current node id.
*/
protected function writeIMG(XMLWriter $writer, DOMElement $node, $id) {
$writer->startElement('ph');
$writer->writeAttribute('id', $id);
$writer->writeAttribute('ctype', 'image');
foreach ($node->attributes as $attribute) {
// @todo - uncomment when issue with Trados/sub elements fixed.
/*
if (in_array($attribute->name, array('title', 'alt'))) {
continue;
}
*/
$writer->writeAttribute($attribute->name, $attribute->value);
}
/*
if ($alt_attribute = $node->getAttribute('alt')) {
$writer->startElement('sub');
$writer->writeAttribute('id', $id . '-img-alt');
$writer->writeAttribute('ctype', 'x-img-alt');
$writer->text($alt_attribute);
$writer->endElement();
$this->elementsCount++;
}
if ($title_attribute = $node->getAttribute('title')) {
$writer->startElement('sub');
$writer->writeAttribute('id', $id . '-img-title');
$writer->writeAttribute('ctype', 'x-img-title');
$writer->text($title_attribute);
$writer->endElement();
$this->elementsCount++;
}
*/
$writer->endElement();
}
/**
* Convert critical characters to HTML entities.
*
* DOMDocument will convert HTML entities to its actual characters. This can
* lead into situation when not allowed characters will appear in the content.
*
* @param string $string
* String to escape.
*
* @return string
* Escaped string.
*/
protected function toEntities($string) {
return str_replace(array('&', '>', '<'), array('&amp;', '&gt;', '&lt;'), $string);
}
}

View File

@@ -0,0 +1,20 @@
name = Export / Import File
description = A translator which allows you to export source data into a file and import the translated in return.
package = Translation Management
core = 7.x
dependencies[] = tmgmt
configure = admin/config/regional/tmgmt_translator
files[] = tmgmt_file.plugin.inc
files[] = tmgmt_file.ui.inc
files[] = tmgmt_file.format.interface.inc
files[] = tmgmt_file.format.xliff.inc
files[] = tmgmt_file.format.html.inc
files[] = tmgmt_file.recursive_iterator.inc
files[] = tmgmt_file.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,195 @@
<?php
/**
* @file
* Module file of the translation management test module.
*/
/**
* Implements hook_tmgmt_translator_plugin_info().
*/
function tmgmt_file_tmgmt_translator_plugin_info() {
return array(
'file' => array(
'label' => t('File translator'),
'description' => t('File translator that exports and imports files.'),
'plugin controller class' => 'TMGMTFileTranslatorPluginController',
'ui controller class' => 'TMGMTFileTranslatorUIController',
),
);
}
/**
* Implements hook_theme().
*/
function tmgmt_file_theme() {
return array(
'tmgmt_file_html_template' => array(
'path' => drupal_get_path('module', 'tmgmt_file') . '/templates',
'template' => 'tmgmt_file_html_template',
),
);
}
/**
* Import form submit callback.
*/
function tmgmt_file_import_form_submit($form, &$form_state) {
// Ensure we have the file uploaded.
$job = $form_state['tmgmt_job'];
$supported_formats = array_keys(tmgmt_file_format_plugin_info());
if ($file = file_save_upload('file', array('file_validate_extensions' => array(implode(' ', $supported_formats))))) {
$extension = pathinfo($file->uri, PATHINFO_EXTENSION);
$controller = tmgmt_file_format_controller($extension);
if ($controller) {
// Validate the file on job.
$validated_job = $controller->validateImport($file->uri, $job);
if (!$validated_job) {
$job->addMessage('Failed to validate file, import aborted.', array(), 'error');
}
elseif ($validated_job->tjid != $job->tjid) {
$job->addMessage('The imported file job id @file_tjid does not match the job id @job_tjid.', array(
'@file_tjid' => $validated_job->tjid,
'@job_tjid' => $job->tjid,
), 'error');
}
else {
try {
// Validation successful, start import.
$job->addTranslatedData($controller->import($file->uri));
$job->addMessage('Successfully imported file.');
} catch (Exception $e) {
$job->addMessage('File import failed with the following message: @message', array('@message' => $e->getMessage()), 'error');
}
}
}
}
foreach ($job->getMessagesSince() as $message) {
// Ignore debug messages.
if ($message->type == 'debug') {
continue;
}
if ($text = $message->getMessage()) {
drupal_set_message(filter_xss($text), $message->type);
}
}
}
/**
* Returns information about file format plugins.
*
* @param $plugin
* (Optional) Name of a plugin/extension.
*
* @return array
* If a plugin name is provided, information about that plugin, an array of
* plugin information otherwise. The information of each plugin consists of
* the label and plugin controller class, keyed by the plugin name which is
* also the extension for that file format.
*/
function tmgmt_file_format_plugin_info($plugin = NULL) {
return _tmgmt_plugin_info('file_format', $plugin);
}
/**
* Returns an array of file format plugin labels.
*/
function tmgmt_file_format_plugin_labels() {
return _tmgmt_plugin_labels('file_format');
}
/**
* Returns the file format plugin controller.
*
* @param $plugin
* (Optional) Name of a plugin/extension.
*
* @return TMGMTFileFormatInterface
* Either a specific file format plugin controller instance or an array of
* available controllers.
*/
function tmgmt_file_format_controller($plugin = NULL) {
return _tmgmt_plugin_controller('file_format', $plugin);
}
/**
* Implements hook_tmgmt_file_format_info().
*/
function tmgmt_file_tmgmt_file_format_plugin_info() {
return array(
'xlf' => array(
'label' => t('XLIFF'),
'plugin controller class' => 'TMGMTFileFormatXLIFF',
),
'html' => array(
'label' => t('HTML'),
'plugin controller class' => 'TMGMTFileFormatHTML',
),
);
}
/**
* Implements hook_tmgmt_job_delete().
*/
function tmgmt_file_tmgmt_job_delete(TMGMTJob $job) {
$translator = $job->getTranslator();
// Ignore jobs that don't have a file translator.
if (!$translator || $translator->plugin != 'file') {
return;
}
// Check if there are any files that need to be deleted.
// @todo There doesn't seem to be an API function for this...
$args = array(
':module' => 'tmgmt_file',
':type' => 'tmgmt_job',
':id' => $job->tjid,
);
$result = db_query('SELECT fid FROM {file_usage} WHERE module = :module and type = :type and id = :id', $args);
$fids = $result->fetchCol();
if (!empty($fids)) {
foreach (file_load_multiple($fids) as $file) {
file_usage_delete($file, 'tmgmt_file', 'tmgmt_job', $job->tjid);
// It is very unlikely that these files are used anywhere else. Delete it.
file_delete($file);
}
}
}
/**
* Implements hook_file_download().
*/
function tmgmt_file_file_download($uri) {
// Get the file record based on the URI. If not in the database just return.
$files = file_load_multiple(array(), array('uri' => $uri));
if (count($files)) {
foreach ($files as $item) {
// Since some database servers sometimes use a case-insensitive comparison
// by default, double check that the filename is an exact match.
if ($item->uri === $uri) {
$file = $item;
break;
}
}
}
if (!isset($file)) {
return;
}
// Check if this file belongs to a job.
$usage_list = file_usage_list($file);
if (!isset($usage_list['tmgmt_file']['tmgmt_job'])) {
return;
}
foreach (tmgmt_job_load_multiple(array_keys($usage_list['tmgmt_file']['tmgmt_job'])) as $job) {
if (tmgmt_job_access('view', $job)) {
// Access is granted.
$headers = file_get_content_headers($file);
return $headers;
}
}
// Returning nothing means access denied unless another module specifically
// grants access.
}

View File

@@ -0,0 +1,60 @@
<?php
/**
* @file
* Provides the file translator plugin controller.
*/
/**
* File translator plugin controller.
*/
class TMGMTFileTranslatorPluginController extends TMGMTDefaultTranslatorPluginController {
/**
* {@inheritdoc}
*/
public function canTranslate(TMGMTTranslator $translator, TMGMTJob $job) {
// Anything can be exported.
return TRUE;
}
/**
* {@inheritdoc}
*/
public function requestTranslation(TMGMTJob $job) {
$name = "JobID" . $job->tjid . '_' . $job->source_language . '_' . $job->target_language;
$export = tmgmt_file_format_controller($job->getSetting('export_format'));
$path = $job->getSetting('scheme') . '://tmgmt_file/' . $name . '.' . $job->getSetting('export_format');
$dirname = dirname($path);
if (file_prepare_directory($dirname, FILE_CREATE_DIRECTORY)) {
$file = file_save_data($export->export($job), $path);
file_usage_add($file, 'tmgmt_file', 'tmgmt_job', $job->tjid);
$job->submitted('Exported file can be downloaded <a href="!link">here</a>.', array('!link' => file_create_url($file->uri)));
}
}
/**
* {@inheritdoc}
*/
public function hasCheckoutSettings(TMGMTJob $job) {
return $job->getTranslator()->getSetting('allow_override');
}
/**
* {@inheritdoc}
*/
public function defaultSettings() {
return array(
'export_format' => 'xlf',
'allow_override' => TRUE,
'scheme' => 'public',
// Making this setting TRUE by default is more appropriate, however we
// need to make it FALSE due to backwards compatibility.
'xliff_processing' => FALSE,
'xliff_cdata' => FALSE,
);
}
}

View File

@@ -0,0 +1,99 @@
<?php
/**
* @file
* Contains RecursiveDOMIterator.
*/
/**
* Class used to iterate through DOMDocument.
*/
class RecursiveDOMIterator implements RecursiveIterator {
/**
* Current position in DOMNodeList.
*
* @var int
*/
protected $position;
/**
* The DOMNodeList with all children to iterate over.
*
* @var DOMNodeList
*/
protected $nodeList;
/**
* Constructor.
*
* @param DOMNode $domNode
* DOMNode to iterate over.
*/
public function __construct(DOMNode $domNode) {
$this->position = 0;
$this->nodeList = $domNode->childNodes;
}
/**
* Returns the current DOMNode.
*
* @return DOMNode
* Current DOMNode object.
*/
public function current() {
return $this->nodeList->item($this->position);
}
/**
* Returns an iterator for the current iterator entry.
*
* @return RecursiveDOMIterator
* Iterator with children elements.
*/
public function getChildren() {
return new self($this->current());
}
/**
* Checks if current element has children.
*
* @return bool
* Has children.
*/
public function hasChildren() {
return $this->current()->hasChildNodes();
}
/**
* Returns the current position.
*
* @return int
* Current position
*/
public function key() {
return $this->position;
}
/**
* Moves the current position to the next element.
*/
public function next() {
$this->position++;
}
/**
* Rewind the Iterator to the first element.
*/
public function rewind() {
$this->position = 0;
}
/**
* Checks if current position is valid.
*
* @return bool
* Is valid.
*/
public function valid() {
return $this->position < $this->nodeList->length;
}
}

View File

@@ -0,0 +1,536 @@
<?php
/**
* @file
* Test cases for the file translator module.
*/
/**
* Basic tests for the file translator.
*/
class TMGMTFileTestCase extends TMGMTBaseTestCase {
static function getInfo() {
return array(
'name' => 'File Translator tests',
'description' => 'Tests the file translator plugin integration.',
'group' => 'Translation Management',
);
}
function setUp() {
parent::setUp(array('tmgmt_file', 'tmgmt_ui'));
$this->loginAsAdmin();
$this->setEnvironment('de');
}
/**
* Test the content processing for XLIFF export and import.
*/
function testXLIFFTextProcessing() {
$translator = $this->createTranslator();
$translator->plugin = 'file';
$translator->settings = array(
'export_format' => 'xlf',
'xliff_processing' => TRUE,
);
$translator->save();
// Get the source text.
$source_text = trim(file_get_contents(drupal_get_path('module', 'tmgmt') . '/tests/testing_html/sample.html'));
// Create the reader instance, it will be used through the tests.
$reader = new XMLReader();
$xliff_elements = array('bpt', 'ept', 'ph', 'x', '#text', '#cdata-section', 'content');
// ==== First test the whole cycle ==== //
$job = $this->createJob();
$job->translator = $translator->name;
$job->addItem('test_html_source', 'test', '1');
// Requesting translation will mask the html.
$job->requestTranslation();
$content = $this->getTransUnitsContent($job);
// Test that the exported trans unit contains only xliff elements.
$reader->XML('<content>' . $content[0]['source'] . '</content>');
while ($reader->read()) {
if (!in_array($reader->name, $xliff_elements)) {
$this->fail(t('The source contains unexpected element %element', array('%element' => $reader->name)));
}
}
$reader->XML('<content>' . $content[0]['target'] . '</content>');
while ($reader->read()) {
if (!in_array($reader->name, $xliff_elements)) {
$this->fail(t('The target contains unexpected element %element', array('%element' => $reader->name)));
}
}
// Import the file, make sure all the html has been revealed and no xliff
// elements are present in the job translation.
$messages = $job->getMessages();
$message = reset($messages);
$translated_file = 'public://tmgmt_file/translated.xlf';
$this->createTranslationFile($message->variables['!link'], 'one paragraph', 'one translated paragraph', $translated_file);
$uri = $job->uri();
$edit = array(
'files[file]' => $translated_file,
);
$this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
// Reset caches and reload job.
entity_get_controller('tmgmt_job')->resetCache();
entity_get_controller('tmgmt_job_item')->resetCache();
$job = tmgmt_job_load($job->tjid);
// Do the comparison of the translation text and the source. It must be the
// same as there was no change done to the translation.
$item_data = $job->getData(array(1, 'dummy', 'deep_nesting'));
$this->assertEqual(trim($item_data[1]['#translation']['#text']), str_replace('one paragraph', 'one translated paragraph', $source_text));
$job_items = $job->getItems();
/** @var TMGMTJobItem $job_item */
$job_item = array_shift($job_items);
// Job item must be in review.
$this->assertTrue($job_item->isNeedsReview());
$this->assertIntegrityCheck($job, FALSE);
// ==== Test integrity check ==== //
$job = $this->createJob();
$job->translator = $translator->name;
$job->addItem('test_html_source', 'test', '1');
$job->requestTranslation();
$messages = $job->getMessages();
$message = reset($messages);
// Get the xml content and remove the element representing <br />. This will
// result in different element counts in the source and target and should
// trigger an error and not import the translation.
$translated_file = 'public://tmgmt_file/translated.xlf';
$this->createTranslationFile($message->variables['!link'], '<x id="tjiid2-4" ctype="lb"/>', '', $translated_file);
$uri = $job->uri();
$edit = array(
'files[file]' => $translated_file,
);
$this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
entity_get_controller('tmgmt_job')->resetCache();
entity_get_controller('tmgmt_job_item')->resetCache();
$job = tmgmt_job_load($job->tjid);
$this->assertIntegrityCheck($job);
// Set the XLIFF processing to FALSE and test it results in the source
// text not being XLIFF processed.
$translator->settings['xliff_processing'] = FALSE;
$translator->save();
$job = $this->createJob();
$job->translator = $translator->name;
$job->addItem('test_html_source', 'test', '1');
$job->requestTranslation();
$targets = $this->getTransUnitsContent($job);
$this->assertEqual(trim(html_entity_decode($targets['0']['source'])), $source_text);
}
/**
* Test the CDATA option for XLIFF export and import.
*/
function testXLIFFCDATA() {
$translator = $this->createTranslator();
$translator->plugin = 'file';
$translator->settings = array(
'export_format' => 'xlf',
'xliff_cdata' => TRUE,
);
$translator->save();
// Get the source text.
$source_text = trim(file_get_contents(drupal_get_path('module', 'tmgmt') . '/tests/testing_html/sample.html'));
// Create a new job.
$job = $this->createJob();
$job->translator = $translator->name;
$job->addItem('test_html_source', 'test', '1');
$job->requestTranslation();
$messages = $job->getMessages();
$message = reset($messages);
$download_url = $message->variables['!link'];
// Get XLIFF content.
$xliff = file_get_contents($download_url);
$dom = new \DOMDocument();
$dom->loadXML($xliff);
$this->assertTrue($dom->schemaValidate(drupal_get_path('module', 'tmgmt_file') . '/xliff-core-1.2-strict.xsd'));
// "Translate" items.
$xml = simplexml_import_dom($dom);
$translated_text = array();
foreach ($xml->file->body->children() as $group) {
foreach ($group->children() as $transunit) {
if ($transunit->getName() == 'trans-unit') {
// The target should be empty.
$this->assertEqual($transunit->target, '');
// Update translations using CDATA.
$node = dom_import_simplexml($transunit->target);
$owner = $node->ownerDocument;
$node->appendChild($owner->createCDATASection($xml->file['target-language'] . '_' . (string) $transunit->source));
// Store the text to allow assertions later on.
$translated_text[(string) $group['id']][(string) $transunit['id']] = (string) $transunit->target;
}
}
}
$translated_file = 'public://tmgmt_file/translated file.xlf';
$xml->asXML($translated_file);
// Import the file and check translation for the "dummy" item.
$uri = $job->uri();
$edit = array(
'files[file]' => $translated_file,
);
$this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
$this->clickLink(t('review'));
foreach ($translated_text[1] as $key => $value) {
$this->assertText(htmlspecialchars($value));
}
}
/**
* Gets trans-unit content from the XLIFF file that has been exported for the
* given job as last.
*/
protected function getTransUnitsContent(TMGMTJob $job) {
$messages = $job->getMessages();
$message = reset($messages);
$download_url = $message->variables['!link'];
$xml_string = file_get_contents($download_url);
$xml = simplexml_load_string($xml_string);
// Register the xliff namespace, required for xpath.
$xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:1.2');
$reader = new XMLReader();
$data = array();
$i = 0;
foreach ($xml->xpath('//xliff:trans-unit') as $unit) {
$reader->XML($unit->source->asXML());
$reader->read();
$data[$i]['source'] = $reader->readInnerXML();
$reader->XML($unit->target->asXML());
$reader->read();
$data[$i]['target'] = $reader->readInnerXML();
$i++;
}
return $data;
}
/**
* Tests export and import for the HTML format.
*/
function testHTML() {
$translator = $this->createTranslator();
$translator->plugin = 'file';
$translator->settings = array(
'export_format' => 'html',
);
$translator->save();
$job = $this->createJob();
$job->translator = $translator->name;
$job->addItem('test_source', 'test', '1');
$job->addItem('test_source', 'test', '2');
$job->requestTranslation();
$messages = $job->getMessages();
$message = reset($messages);
$download_url = $message->variables['!link'];
// "Translate" items.
$xml = simplexml_load_file($download_url);
$translated_text = array();
foreach ($xml->body->children() as $group) {
for ($i = 0; $i < $group->count(); $i++) {
// This does not actually override the whole object, just the content.
$group->div[$i] = (string) $xml->head->meta[3]['content'] . '_' . (string) $group->div[$i];
// Store the text to allow assertions later on.
$translated_text[(string) $group['id']][(string) $group->div[$i]['id']] = (string) $group->div[$i];
}
}
$translated_file = 'public://tmgmt_file/translated.html';
$xml->asXML($translated_file);
$this->importFile($translated_file, $translated_text, $job);
}
/**
* Tests import and export for the XLIFF format.
*/
function testXLIFF() {
$translator = $this->createTranslator();
$translator->plugin = 'file';
$translator->settings = array(
'export_format' => 'xlf',
);
$translator->save();
// Set multiple data items for the source.
variable_set('tmgmt_test_source_data', array(
'dummy' => array(
'deep_nesting' => array(
'#text' => file_get_contents(drupal_get_path('module', 'tmgmt') . '/tests/testing_html/sample.html') . ' @id.',
'#label' => 'Label of deep nested item @id',
),
),
'another_item' => array(
'#text' => 'Text of another item @id.',
'#label' => 'Label of another item @id.',
),
));
$job = $this->createJob();
$job->translator = $translator->name;
$first_item = $job->addItem('test_source', 'test', '1');
// Keep the first item data for later use.
$first_item_data = tmgmt_flatten_data($first_item->getData());
$job->addItem('test_source', 'test', '2');
$job->requestTranslation();
$messages = $job->getMessages();
$message = reset($messages);
$download_url = $message->variables['!link'];
$xliff = file_get_contents($download_url);
$dom = new DOMDocument();
$dom->loadXML($xliff);
$this->assertTrue($dom->schemaValidate(drupal_get_path('module', 'tmgmt_file') . '/xliff-core-1.2-strict.xsd'));
// "Translate" items.
$xml = simplexml_import_dom($dom);
$translated_text = array();
foreach ($xml->file->body->children() as $group) {
foreach ($group->children() as $transunit) {
if ($transunit->getName() == 'trans-unit') {
// The target should be empty.
$this->assertEqual($transunit->target, '');
$transunit->target = $xml->file['target-language'] . '_' . (string) $transunit->source;
// Store the text to allow assertions later on.
$translated_text[(string) $group['id']][(string) $transunit['id']] = (string) $transunit->target;
}
}
}
// Change the job id to a non-existing one and try to import it.
$wrong_xml = clone $xml;
$wrong_xml->file->header->{'phase-group'}->phase['job-id'] = 500;
$wrong_file = 'public://tmgmt_file/wrong_file.xlf';
$wrong_xml->asXML($wrong_file);
$uri = $job->uri();
$edit = array(
'files[file]' => $wrong_file,
);
$this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
$this->assertText(t('Failed to validate file, import aborted.'));
// Change the job id to a wrong one and try to import it.
$wrong_xml = clone $xml;
$second_job = $this->createJob();
$second_job->translator = $translator->name;
// We need to add the elements count value into settings, otherwise the
// validation will fail on integrity check.
$second_job->settings['xliff_validation'][1] = 0;
$second_job->settings['xliff_validation'][2] = 0;
$second_job->save();
$wrong_xml->file->header->{'phase-group'}->phase['job-id'] = $second_job->tjid;
$wrong_file = 'public://tmgmt_file/wrong_file.xlf';
$wrong_xml->asXML($wrong_file);
$uri = $job->uri();
$edit = array(
'files[file]' => $wrong_file,
);
$this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
$this->assertRaw(t('The imported file job id @file_tjid does not match the job id @job_tjid.', array(
'@file_tjid' => $second_job->tjid,
'@job_tjid' => $job->tjid,
)));
$translated_file = 'public://tmgmt_file/translated file.xlf';
$xml->asXML($translated_file);
// Import the file and accept translation for the "dummy" item.
$uri = $job->uri();
$edit = array(
'files[file]' => $translated_file,
);
$this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
$this->clickLink(t('review'));
$this->drupalPostAJAX(NULL, NULL, array('reviewed-dummy|deep_nesting' => '✓'));
// Update the translation for "another" item and import.
$xml->file->body->group[0]->{'trans-unit'}[1]->target = $xml->file->body->group[0]->{'trans-unit'}[1]->target . ' updated';
$xml->asXML($translated_file);
$uri = $job->uri();
$edit = array(
'files[file]' => $translated_file,
);
$this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
// At this point we must have the "dummy" item accepted and intact. The
// "another" item must have updated translation.
$this->clickLink(t('review'));
$this->assertFieldByName('dummy|deep_nesting[translation]', 'de_' . $first_item_data['dummy][deep_nesting']['#text']);
$this->assertFieldByName('another_item[translation]', 'de_' . $first_item_data['another_item']['#text'] . ' updated');
// Now finish the import/save as completed process doing another extra
// import. The extra import will test that a duplicate import of the same
// file does not break the process.
$this->importFile($translated_file, $translated_text, $job);
$this->assertNoText(t('Import translated file'));
// Create a job, assign to the file translator and delete before attaching
// a file.
$other_job = $this->createJob();
$other_job->translator = $translator->name;
$other_job->save();
$other_job->delete();
// Make sure the file of the other job still exists.
$response = drupal_http_request($download_url);
$this->assertEqual(200, $response->code);
// Delete the job and then make sure that the file has been deleted.
$job->delete();
$response = drupal_http_request($download_url);
$this->assertEqual(404, $response->code);
}
/**
* Tests storing files in the private file system.
*/
function testPrivate() {
// Enable the private file system.
variable_set('file_private_path', variable_get('file_public_path') . '/private');
// Create a translator using the private file system.
// @todo: Test the configuration UI.
$translator = $this->createTranslator();
$translator->plugin = 'file';
$translator->settings = array(
'export_format' => 'xlf',
'scheme' => 'private',
);
$translator->save();
$job = $this->createJob();
$job->translator = $translator->name;
$job->addItem('test_source', 'test', '1');
$job->addItem('test_source', 'test', '2');
$job->requestTranslation();
$messages = $job->getMessages();
$message = reset($messages);
$download_url = $message->variables['!link'];
$this->drupalGet($download_url);
// Verify that the URL is served using the private file system and the
// access checks work.
$this->assertTrue(preg_match('|system/files|', $download_url));
$this->assertResponse(200);
$this->drupalLogout();
// Verify that access is now protected.
$this->drupalGet($download_url);
$this->assertResponse(403);
}
protected function importFile($translated_file, $translated_text, TMGMTJob $job) {
// To test the upload form functionality, navigate to the edit form.
$uri = $job->uri();
$edit = array(
'files[file]' => $translated_file,
);
$this->drupalPost($uri['path'] . '/manage', $edit, t('Import'));
// Make sure the translations have been imported correctly.
$this->assertNoText(t('In progress'));
// @todo: Enable this assertion once new releases for views and entity
// module are out.
//$this->assertText(t('Needs review'));
// Review both items.
$this->clickLink(t('review'));
foreach ($translated_text[1] as $key => $value) {
$this->assertText(check_plain($value));
}
foreach ($translated_text[2] as $key => $value) {
$this->assertNoText(check_plain($value));
}
$this->drupalPost(NULL, array(), t('Save as completed'));
// Review both items.
$this->clickLink(t('review'));
foreach ($translated_text[1] as $key => $value) {
$this->assertNoText(check_plain($value));
}
foreach ($translated_text[2] as $key => $value) {
$this->assertText(check_plain($value));
}
$this->drupalPost(NULL, array(), t('Save as completed'));
// @todo: Enable this assertion once new releases for views and entity
// module are out.
//$this->assertText(t('Accepted'));
$this->assertText(t('Finished'));
$this->assertNoText(t('Needs review'));
}
/**
* Creates a translated XLIFF file based on the replacement definition.
*
* @param string $source_file
* Source file name.
* @param $search
* String to search in the source.
* @param $replace
* String to replace it with in the target.
* @param $translated_file
* Name of the file to write.
*/
protected function createTranslationFile($source_file, $search, $replace, $translated_file) {
$xml_string = file_get_contents($source_file);
preg_match('/<source xml:lang="en">(.+)<\/source>/s', $xml_string, $matches);
$target = str_replace($search, $replace, $matches[1]);
if ($replace) {
$this->assertTrue(strpos($target, $replace) !== FALSE, 'String replaced in translation');
}
$translated_xml_string = str_replace('<target xml:lang="de"/>', '<target xml:lang="de">' . $target . '</target>', $xml_string);
file_put_contents($translated_file, $translated_xml_string);
}
/**
* Asserts import integrity for a job.
*
* @param TMGMTJob $job
* The job to check.
* @param bool $expected
* (optional) If an integrity failed message is expected or not, defaults
* to FALSE.
*/
protected function assertIntegrityCheck(TMGMTJob $job, $expected = TRUE) {
$integrity_check_failed = FALSE;
/** @var TMGMTMessage $message */
foreach ($job->getMessages() as $message) {
if ($message->getMessage() == t('Failed to validate semantic integrity of %key element. Please check also the HTML code of the element in the review process.', array('%key' => 'dummy][deep_nesting'))) {
$integrity_check_failed = TRUE;
break;
}
}
// Check if the message was found or not, based on the expected argument.
if ($expected) {
$this->assertTrue($integrity_check_failed, 'The validation of semantic integrity must fail.');
}
else {
$this->assertFalse($integrity_check_failed, 'The validation of semantic integrity must not fail.');
}
}
}

View File

@@ -0,0 +1,115 @@
<?php
/**
* @file
* Please supply a file description.
*/
/**
* File translator plugin controller.
*/
class TMGMTFileTranslatorUIController extends TMGMTDefaultTranslatorUIController {
/**
* {@inheritdoc}
*/
public function pluginSettingsForm($form, &$form_state, TMGMTTranslator $translator, $busy = FALSE) {
$form['export_format'] = array(
'#type' => 'radios',
'#title' => t('Export to'),
'#options' => tmgmt_file_format_plugin_labels(),
'#default_value' => $translator->getSetting('export_format'),
'#description' => t('Please select the format you want to export data.'),
);
$form['xliff_cdata'] = array(
'#type' => 'checkbox',
'#title' => t('XLIFF CDATA'),
'#description' => t('Check to use CDATA for import/export.'),
'#default_value' => $translator->getSetting('xliff_cdata'),
);
$form['xliff_processing'] = array(
'#type' => 'checkbox',
'#title' => t('Extended XLIFF processing'),
'#description' => t('Check to further process content semantics and mask HTML tags instead just escaping it.'),
'#default_value' => $translator->getSetting('xliff_processing'),
);
$form['xliff_message'] = array(
'#type' => 'item',
'#markup' => t('By selecting CDATA option, XLIFF processing will be ignored.'),
'#prefix' => '<div class="messages warning">',
'#suffix' => '</div>',
);
$form['allow_override'] = array(
'#type' => 'checkbox',
'#title' => t('Allow to override the format per job'),
'#default_value' => $translator->getSetting('allow_override'),
);
// Any visible, writeable wrapper can potentially be used for the files
// directory, including a remote file system that integrates with a CDN.
foreach (file_get_stream_wrappers(STREAM_WRAPPERS_WRITE_VISIBLE) as $scheme => $info) {
$options[$scheme] = check_plain($info['description']);
}
if (!empty($options)) {
$form['scheme'] = array(
'#type' => 'radios',
'#title' => t('Download method'),
'#default_value' => $translator->getSetting('scheme'),
'#options' => $options,
'#description' => t('Choose the location where exported files should be stored. The usage of a protected location (e.g. private://) is recommended to prevent unauthorized access.'),
);
}
return parent::pluginSettingsForm($form, $form_state, $translator);
}
/**
* {@inheritdoc}
*/
public function checkoutSettingsForm($form, &$form_state, TMGMTJob $job) {
if ($job->getTranslator()->getSetting('allow_override')) {
$form['export_format'] = array(
'#type' => 'radios',
'#title' => t('Export to'),
'#options' => tmgmt_file_format_plugin_labels(),
'#default_value' => $job->getTranslator()->getSetting('export_format'),
'#description' => t('Please select the format you want to export data.'),
);
}
return parent::checkoutSettingsForm($form, $form_state, $job);
}
/**
* {@inheritdoc}
*/
public function checkoutInfo(TMGMTJob $job) {
// If the job is finished, it's not possible to import translations anymore.
if ($job->isFinished()) {
return parent::checkoutInfo($job);
}
$form = array(
'#type' => 'fieldset',
'#title' => t('Import translated file'),
);
$supported_formats = array_keys(tmgmt_file_format_plugin_info());
$form['file'] = array(
'#type' => 'file',
'#title' => t('File file'),
'#size' => 50,
'#description' => t('Supported formats: @formats.', array('@formats' => implode(', ', $supported_formats))),
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Import'),
'#submit' => array('tmgmt_file_import_form_submit'),
);
return $this->checkoutInfoWrapper($job, $form);
}
}

View File

@@ -0,0 +1,59 @@
TMGMT Local Translator
----------------------
A user interface to execute the actual translation of TMGMT source elements.
Includes management capabilities for handling translations and users.
Requirements
------------
Local Translator is a translation plugin for TMGMT and is included in TMGMT core.
Basic Concepts
--------------
Acts as a translator plugin for TMGMT. Can be used to do manual local
translation. In addition it offers some management capabilities to handle the
assignment of jobs to users.
The local translator adds two permissions:
- Provide translation services
The user can assign jobs to himself (assuming he has the right skills) and
execute the translation. Adds a 'Translate' link to the User menu.
- Administer translation tasks
Assign jobs to other users for translation. Adds a 'Manage Translate Tasks'
to the User Menu.
Getting started
---------------
In TMGMT, a translation job can be sent to the local translator. In the checkout
settings, the plugin offers the possibility to assign the job to a specific user.
Only users with the required language skills are listed at this moment. If no
person is selected, the job will be moved to the 'unassigned' task list for later
treatment.
Following the 'Translate' link in the User Menu, the user finds a listing
of the tasks assigned to him as well as eligible tasks to assign to himself,
depending on his skills. Assigned tasks will show a 'translate' link in the
action column. Follow it to get a list of the task items to be translated.
Choose to translate one item to get to the actual translation page.
It lists two panes for each data item contained in the task item. One showing
the text in the original language. The second one for writing in the translation.
Each line sports a check button to its right. Once the translation is done, the
data item can be checked off as complete. This check is purely informational
and has no functional consequences.
A translation task item can be saved at any time for later rework. It will show
up as pending or translated depending on the state of the check mark for each
item.
Once the data items are all translated, the 'Save as completed' button will
finalize the task and send it back to TMGMT core for further processing. Please
note: Completing a task item is independent of the state of the checkboxes.

View File

@@ -0,0 +1,39 @@
<?php
/**
* @file
* Contains the task controller.
*/
/**
* Controller class for the local task entity.
*
* @ingroup tmgmt_local_task
*/
class TMGMTLocalTaskController extends EntityAPIController {
/**
* {@inheritdoc}
*/
public function save($entity, DatabaseTransaction $transaction = NULL) {
$entity->changed = REQUEST_TIME;
return parent::save($entity, $transaction);
}
/**
* {@inheritdoc}
*/
public function delete($ids, $transaction = NULL) {
parent::delete($ids, $transaction);
$query = new EntityFieldQuery();
$result = $query
->entityCondition('entity_type', 'tmgmt_local_task_item')
->propertyCondition('tltid', $ids)
->execute();
if (!empty($result['tmgmt_local_task_item'])) {
entity_delete_multiple('tmgmt_local_task_item', array_keys($result['tmgmt_local_task_item']));
}
}
}

View File

@@ -0,0 +1,77 @@
<?php
/**
* @file
* Contains the task item controller.
*/
/**
* Controller class for the local task entity.
*
* @ingroup tmgmt_local_task
*/
class TMGMTLocalTaskItemController extends EntityAPIController {
/**
* {@inheritdoc}
*
* @todo Eliminate the need to flatten and unflatten the TaskItem data.
*/
public function save($entity, DatabaseTransaction $transaction = NULL) {
// Consider everything translated when the job item is translated.
if ($entity->isCompleted()) {
$entity->count_untranslated = 0;
$entity->count_translated = count(tmgmt_flatten_data($entity->data));
$entity->count_completed = 0;
}
// Consider everything completed if the job is completed.
elseif ($entity->isClosed()) {
$entity->count_untranslated = 0;
$entity->count_translated = 0;
$entity->count_completed = count(tmgmt_flatten_data($entity->data));
}
// Count the data item states.
else {
// Start with assuming that all data is untranslated, then go through it
// and count translated data.
$entity->count_untranslated = count(array_filter(tmgmt_flatten_data($entity->getJobItem()->getData()), '_tmgmt_filter_data'));
$entity->count_translated = 0;
$entity->count_completed = 0;
$this->count($entity->data, $entity);
}
return parent::save($entity, $transaction);
}
/**
* Parse all data items recursively and sums up the counters for
* accepted, translated and pending items.
*
* @param $item
* The current data item.
* @param $entity
* The job item the count should be calculated.
*/
protected function count(&$item, $entity) {
if (!empty($item['#text'])) {
if (_tmgmt_filter_data($item)) {
// Set default states if no state is set.
if (!isset($item['#status'])) {
$item['#status'] = TMGMT_DATA_ITEM_STATE_UNTRANSLATED;
}
switch ($item['#status']) {
case TMGMT_DATA_ITEM_STATE_TRANSLATED:
$entity->count_untranslated--;
$entity->count_translated++;
break;
}
}
}
else {
foreach (element_children($item) as $key) {
$this->count($item[$key], $entity);
}
}
}
}

View File

@@ -0,0 +1,76 @@
<?php
/**
* Entity UI controller for the local task entity.
*/
class TMGMTLocalTaskUIController extends EntityDefaultUIController {
/**
* {@inheritdoc}
*/
public function hook_menu() {
$id_count = count(explode('/', $this->path));
$wildcard = isset($this->entityInfo['admin ui']['menu wildcard']) ? $this->entityInfo['admin ui']['menu wildcard'] : '%entity_object';
$items[$this->path . '/' . $wildcard] = array(
'title callback' => 'entity_label',
'title arguments' => array($this->entityType, $id_count),
'page callback' => 'tmgmt_local_task_view',
'page arguments' => array($id_count),
'load arguments' => array($this->entityType),
'access callback' => 'entity_access',
'access arguments' => array('view', $this->entityType, $id_count),
'file' => 'tmgmt_local.pages.inc',
'file path' => drupal_get_path('module', 'tmgmt_local') . '/includes',
);
$items[$this->path . '/' . $wildcard . '/delete'] = array(
'page callback' => 'drupal_get_form',
'page arguments' => array($this->entityType . '_operation_form', $this->entityType, $id_count, $id_count + 1),
'load arguments' => array($this->entityType),
'access callback' => 'entity_access',
'access arguments' => array('delete', $this->entityType, $id_count),
'type' => MENU_CALLBACK,
);
$items[$this->path . '/' . $wildcard . '/unassign'] = array(
'page callback' => 'drupal_get_form',
'page arguments' => array($this->entityType . '_operation_form', $this->entityType, $id_count, $id_count + 1),
'load arguments' => array($this->entityType),
'access callback' => 'entity_access',
'access arguments' => array('unassign', $this->entityType, $id_count),
'type' => MENU_CALLBACK,
);
return $items;
}
/**
* {@inheritdoc}
*/
public function operationForm($form, &$form_state, $entity, $op) {
switch ($op) {
case 'delete':
$confirm_question = t('Are you sure you want to delete the translation task %label?', array('%label' => $entity->label()));
return confirm_form($form, $confirm_question, $this->path);
case 'unassign':
$confirm_question = t('Are you sure you want to unassign from the translation task %label?', array('%label' => $entity->label()));
return confirm_form($form, $confirm_question, $this->path);
}
drupal_not_found();
exit;
}
/**
* {@inheritdoc}
*/
public function applyOperation($op, $entity) {
switch ($op) {
case 'delete':
$entity->delete();
return t('Deleted the translation local task %label.', array('%label' => $entity->label()));
case 'unassign':
$entity->unassign();
$entity->save();
return t('Unassigned from translation local task %label.', array('%label' => $entity->label()));
}
return FALSE;
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Entity UI controller for the local task item entity.
*/
class TMGMTLocalTaskItemUIController extends EntityDefaultUIController {
/**
* {@inheritdoc}
*/
public function hook_menu() {
$id_count = count(explode('/', $this->path));
$items[$this->path . '/%tmgmt_local_task/item/%tmgmt_local_task_item'] = array(
'title callback' => 'entity_label',
'title arguments' => array($this->entityType, $id_count + 2),
'page callback' => 'tmgmt_local_task_item_view',
'page arguments' => array($id_count + 2),
'load arguments' => array($this->entityType),
'access callback' => 'entity_access',
'access arguments' => array('view', $this->entityType, $id_count + 2),
'file' => 'tmgmt_local.pages.inc',
'file path' => drupal_get_path('module', 'tmgmt_local') . '/includes',
);
return $items;
}
}

View File

@@ -0,0 +1,387 @@
<?php
/*
* @file
* Entity class.
*/
/**
* Entity class for the local task entity.
*
* @ingroup tmgmt_local_task
*/
class TMGMTLocalTask extends Entity {
/**
* Translation local task identifier.
*
* @var int
*/
public $tltid;
/**
* The user id of the creator of the task.
*
* @var int
*/
public $uid;
/**
* The time when the task was created as a timestamp.
*
* @var int
*/
public $created = REQUEST_TIME;
/**
* The time when the task was changed as a timestamp.
*
* @var int
*/
public $changed;
/**
* A title of this task.
*
* @var string
*/
public $title;
/**
* The user id of the assigned translator.
*
* @var int
*/
public $tuid;
/**
* Translation job.
*
* @var int
*/
public $tjid;
/**
* Current status of the task.
*
* @var int
*/
public $status = TMGMT_LOCAL_TASK_STATUS_UNASSIGNED;
/**
* Counter for how many times task was returned to translator.
*
* @var int
*/
public $loop_count;
/**
* {@inheritdoc}
*/
public function __construct(array $values = array(), $entity_type = 'tmgmt_local_task') {
parent::__construct($values, $entity_type);
}
/*
* {@inheritdoc}
*/
public function defaultUri() {
return array('path' => 'translate/' . $this->tltid);
}
/**
* {@inheritdoc}
*/
protected function defaultLabel() {
if (empty($this->tuid)) {
if (empty($this->title)) {
return t('Task for @job', array('@job' => $this->getJob()->label()));
}
else {
return $this->title;
}
}
else {
if (empty($this->title)) {
return t('Task for @job assigned to @translator', array('@job' => $this->getJob()->label(), '@translator' => entity_label('user', user_load($this->tuid))));
}
else {
return t('@title assigned to @translator', array('@title' => $this->title, '@translator' => entity_label('user', user_load($this->tuid))));
}
}
}
/**
* {@inheritdoc}
*/
public function buildContent($view_mode = 'full', $langcode = NULL) {
$content = entity_ui_get_form('tmgmt_local_task', $this);
return entity_get_controller($this->entityType)->buildContent($this, $view_mode, $langcode, $content);
}
/**
* Return the corresponding translation job.
*
* @return TMGMTJob
*/
public function getJob() {
return tmgmt_job_load($this->tjid);
}
/**
* Assign translation task to passed user.
*
* @param object $user
* User object.
*/
public function assign($user) {
$this->incrementLoopCount(TMGMT_LOCAL_TASK_STATUS_PENDING, $user->uid);
$this->tuid = $user->uid;
$this->status = TMGMT_LOCAL_TASK_STATUS_PENDING;
}
/**
* Unassign translation task.
*/
public function unassign() {
// We also need to increment loop count when unassigning.
$this->incrementLoopCount(TMGMT_LOCAL_TASK_STATUS_UNASSIGNED, 0);
$this->tuid = 0;
$this->status = TMGMT_LOCAL_TASK_STATUS_UNASSIGNED;
}
/**
* Returns all job items attached to this task.
*
* @return array
* An array of translation job items.
*/
public function getItems($conditions = array()) {
$query = new EntityFieldQuery();
$query->entityCondition('entity_type', 'tmgmt_local_task_item');
$query->propertyCondition('tltid', $this->tltid);
foreach ($conditions as $key => $condition) {
if (is_array($condition)) {
$operator = isset($condition['operator']) ? $condition['operator'] : '=';
$query->propertyCondition($key, $condition['value'], $operator);
}
else {
$query->propertyCondition($key, $condition);
}
}
$results = $query->execute();
if (!empty($results['tmgmt_local_task_item'])) {
return entity_load('tmgmt_local_task_item', array_keys($results['tmgmt_local_task_item']));
}
return array();
}
/**
* Create a task item for this task and the given job item.
*
* @param TMGMTJobItem $job_item
* The job item.
*/
public function addTaskItem(TMGMTJobItem $job_item) {
// Save the task to get an id.
if (empty($this->tltid)) {
$this->save();
}
$local_task = entity_create('tmgmt_local_task_item', array(
'tltid' => $this->identifier(),
'tjiid' => $job_item->identifier(),
));
$local_task->save();
return $local_task;
}
/**
* Returns the status of the task. Can be one of the task status constants.
*
* @return int
* The status of the task or NULL if it hasn't been set yet.
*/
public function getStatus() {
return $this->status;
}
/**
* Updates the status of the task.
*
* @param $status
* The new status of the task. Has to be one of the task status constants.
* @param $message
* (Optional) The log message to be saved along with the status change.
* @param $variables
* (Optional) An array of variables to replace in the message on display.
*
* @return int
* The updated status of the task if it could be set.
*
* @see TMGMTJob::addMessage()
*/
public function setStatus($status) {
// Return TRUE if the status could be set. Return FALSE otherwise.
if (array_key_exists($status, tmgmt_local_task_statuses())) {
$this->incrementLoopCount($status, $this->tuid);
$this->status = $status;
$this->save();
}
return $this->status;
}
/**
* Checks whether the passed value matches the current status.
*
* @param $status
* The value to check the current status against.
*
* @return boolean
* TRUE if the passed status matches the current status, FALSE otherwise.
*/
public function isStatus($status) {
return $this->getStatus() == $status;
}
/**
* Checks whether the user described by $account is the author of this task.
*
* @param $account
* (Optional) A user object. Defaults to the currently logged in user.
*/
public function isAuthor($account = NULL) {
$account = isset($account) ? $account : $GLOBALS['user'];
return $this->uid == $account->uid;
}
/**
* Returns whether the status of this task is 'unassigned'.
*
* @return boolean
* TRUE if the status is 'unassigned', FALSE otherwise.
*/
public function isUnassigned() {
return $this->isStatus(TMGMT_LOCAL_TASK_STATUS_UNASSIGNED);
}
/**
* Returns whether the status of this task is 'pending'.
*
* @return boolean
* TRUE if the status is 'pending', FALSE otherwise.
*/
public function isPending() {
return $this->isStatus(TMGMT_LOCAL_TASK_STATUS_PENDING);
}
/**
* Returns whether the status of this task is 'completed'.
*
* @return boolean
* TRUE if the status is 'completed', FALSE otherwise.
*/
public function isCompleted() {
return $this->isStatus(TMGMT_LOCAL_TASK_STATUS_COMPLETED);
}
/**
* Returns whether the status of this task is 'rejected'.
*
* @return boolean
* TRUE if the status is 'rejected', FALSE otherwise.
*/
public function isRejected() {
return $this->isStatus(TMGMT_LOCAL_TASK_STATUS_REJECTED);
}
/**
* Returns whether the status of this task is 'closed'.
*
* @return boolean
* TRUE if the status is 'closed', FALSE otherwise.
*/
public function isClosed() {
return $this->isStatus(TMGMT_LOCAL_TASK_STATUS_CLOSED);
}
/**
* Count of all translated data items.
*
* @return
* Translated count
*/
public function getCountTranslated() {
return tmgmt_local_task_statistic($this, 'count_translated');
}
/**
* Count of all untranslated data items.
*
* @return
* Translated count
*/
public function getCountUntranslated() {
return tmgmt_local_task_statistic($this, 'count_untranslated');
}
/**
* Count of all completed data items.
*
* @return
* Translated count
*/
public function getCountCompleted() {
return tmgmt_local_task_statistic($this, 'count_completed');
}
/**
* Sums up all word counts of this task job items.
*
* @return
* The sum of all accepted counts
*/
public function getWordCount() {
return tmgmt_local_task_statistic($this, 'word_count');
}
/**
* Returns loop count of a task.
*
* @return int
* Task loop count.
*/
public function getLoopCount() {
return $this->loop_count;
}
/**
* Increment loop_count property depending on current status, new status and
* new translator.
*
* @param int $newStatus
* New status of task.
* @param int $new_tuid
* New translator uid.
*/
public function incrementLoopCount($newStatus, $new_tuid) {
if ($this->status == TMGMT_LOCAL_TASK_STATUS_PENDING
&& $newStatus == TMGMT_LOCAL_TASK_STATUS_PENDING
&& $this->tuid != $new_tuid) {
++$this->loop_count;
}
else if ($this->status != TMGMT_LOCAL_TASK_STATUS_UNASSIGNED
&& $newStatus == TMGMT_LOCAL_TASK_STATUS_UNASSIGNED) {
++$this->loop_count;
}
else if ($this->status != TMGMT_LOCAL_TASK_STATUS_UNASSIGNED
&& $this->status != TMGMT_LOCAL_TASK_STATUS_PENDING
&& $newStatus == TMGMT_LOCAL_TASK_STATUS_PENDING) {
++$this->loop_count;
}
}
}

View File

@@ -0,0 +1,252 @@
<?php
/*
* @file
* Entity class.
*/
/**
* Entity class for the local task item entity.
*
* @ingroup tmgmt_local_task
*/
class TMGMTLocalTaskItem extends Entity {
/**
* Translation local task item identifier.
*
* @var int
*/
public $tltiid;
/**
* The task identifier.
*
* @var int
*/
public $tltid;
/**
* Translation job item.
*
* @var int
*/
public $tjiid;
/**
* Current status of the task.
*
* @var int
*/
public $status;
/**
* Translated data and data item status.
*
* @var array
*/
public $data = array();
/**
* Counter for all untranslated data items.
*
* @var integer
*/
public $count_untranslated = 0;
/**
* Counter for all translated data items.
*
* @var integer
*/
public $count_translated = 0;
/**
* Counter for all completed data items.
*
* @var integer
*/
public $count_completed = 0;
/**
* {@inheritdoc}
*/
public function __construct(array $values = array(), $entity_type = 'tmgmt_local_task_item') {
parent::__construct($values, $entity_type);
}
/*
* {@inheritdoc}
*/
public function defaultUri() {
return array('path' => 'translate/' . $this->tltid . '/item/' . $this->tltiid);
}
/**
* {@inheritdoc}
*/
protected function defaultLabel() {
if ($job_item = $this->getJobItem()) {
return $job_item->label();
}
return t('Missing job item');
}
/**
* Returns the translation task.
*
* @return TMGMTLocalTask
*/
public function getTask() {
return entity_load_single('tmgmt_local_task', $this->tltid);
}
/**
* Returns the translation job item.
*
* @return TMGMTJobItem
*/
public function getJobItem() {
return entity_load_single('tmgmt_job_item', $this->tjiid);
}
/**
* {@inheritdoc}
*/
public function buildContent($view_mode = 'full', $langcode = NULL) {
$content = drupal_get_form('tmgmt_local_translation_form', $this);
return entity_get_controller($this->entityType)->buildContent($this, $view_mode, $langcode, $content);
}
/**
* Returns TRUE if the local task is pending.
*
* @return bool
* TRUE if the local task item is untranslated.
*/
public function isPending() {
return $this->status == TMGMT_LOCAL_TASK_ITEM_STATUS_PENDING;
}
/**
* Returns TRUE if the local task is translated (fully translated).
*
* @return bool
* TRUE if the local task item is translated.
*/
public function isCompleted() {
return $this->status == TMGMT_LOCAL_TASK_ITEM_STATUS_COMPLETED;
}
/**
* Rreturns TRUE if the local task is closed (translated and accepted).
*
* @return bool
* TRUE if the local task item is translated and accepted.
*/
public function isClosed() {
return $this->status == TMGMT_LOCAL_TASK_ITEM_STATUS_CLOSED;
}
/**
* Sets the task item status to completed.
*/
public function completed() {
$this->status = TMGMT_LOCAL_TASK_ITEM_STATUS_COMPLETED;
}
/**
* Sets the task item status to closed.
*/
public function closed() {
$this->status = TMGMT_LOCAL_TASK_ITEM_STATUS_CLOSED;
}
/**
* Updates the values for a specific substructure in the data array.
*
* The values are either set or updated but never deleted.
*
* @param $key
* Key pointing to the item the values should be applied.
* The key can be either be an array containing the keys of a nested array
* hierarchy path or a string with '][' or '|' as delimiter.
* @param $values
* Nested array of values to set.
*/
public function updateData($key, $values = array()) {
foreach ($values as $index => $value) {
// In order to preserve existing values, we can not aplly the values array
// at once. We need to apply each containing value on its own.
// If $value is an array we need to advance the hierarchy level.
if (is_array($value)) {
$this->updateData(array_merge(tmgmt_ensure_keys_array($key), array($index)), $value);
}
// Apply the value.
else {
drupal_array_set_nested_value($this->data, array_merge(tmgmt_ensure_keys_array($key), array($index)), $value);
}
}
}
/**
* Array of translations.
*
* The structure is similar to the form API in the way that it is a possibly
* nested array with the following properties whose presence indicate that the
* current element is a text that might need to be translated.
*
* - #text: The translated text of the corresponding entry in the job item.
* - #status: The status of the translation.
*
* The key can be an alphanumeric string.
*
* @param array $key
* If present, only the subarray identified by key is returned.
* @param string $index
* Optional index of an attribute below $key.
*
* @return array
* A structured data array.
*/
public function getData(array $key = array(), $index = NULL) {
if (empty($key)) {
return $this->data;
}
if ($index) {
$key = array_merge($key, array($index));
}
return drupal_array_get_nested_value($this->data, $key);
}
/**
* Count of all translated data items.
*
* @return
* Translated count
*/
public function getCountTranslated() {
return $this->count_translated;
}
/**
* Count of all untranslated data items.
*
* @return
* Translated count
*/
public function getCountUntranslated() {
return $this->count_untranslated;
}
/**
* Count of all completed data items.
*
* @return
* Translated count
*/
public function getCountCompleted() {
return $this->count_completed;
}
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* @file
* Contains the metadata controller classes for the local task translatio
* entity.
*/
/**
* Metadata controller for the local task entity.
*/
class TMGMTLocalTaskMetadataController extends EntityDefaultMetadataController {
/**
* {@inheritdoc}
*/
public function entityPropertyInfo() {
$info = parent::entityPropertyInfo();
// $info = _tmgmt_override_property_description($info, $this->type);
$properties = &$info[$this->type]['properties'];
// Make the created and changed property appear as date.
$properties['changed']['type'] = $properties['created']['type'] = 'date';
// Add the options list for the defined status constants.
$properties['status']['options list'] = 'tmgmt_local_task_statuses';
// Link the job property to the corresponding job entity.
$properties['job'] = array(
'label' => t('Translation Job'),
'type' => 'tmgmt_job',
'description' => t('Corresponding job entity of the translation task.'),
'setter callback' => 'entity_property_verbatim_set',
'setter permission' => 'administer tmgmt',
'required' => TRUE,
'schema field' => 'tjid',
);
// Link the author property to the corresponding user entity.
$properties['author'] = array(
'label' => t('Author'),
'type' => 'user',
'description' => t('The author of the translation task.'),
'setter callback' => 'entity_property_verbatim_set',
'setter permission' => 'administer tmgmt',
'required' => TRUE,
'schema field' => 'uid',
);
// Link the author property to the corresponding user entity.
$properties['translator'] = array(
'label' => t('Translator'),
'type' => 'user',
'description' => t('The assigned translator for translation task.'),
'setter callback' => 'entity_property_verbatim_set',
'setter permission' => 'administer tmgmt',
'required' => TRUE,
'schema field' => 'tuid',
);
return $info;
}
}
/**
* Metadata controller for the local task entity.
*/
class TMGMTLocalTaskItemMetadataController extends EntityDefaultMetadataController {
/**
* {@inheritdoc}
*/
public function entityPropertyInfo() {
$info = parent::entityPropertyInfo();
$properties = &$info[$this->type]['properties'];
// Add the options list for the defined status constants.
$properties['status']['options list'] = 'tmgmt_local_task_item_statuses';
// Link the job property to the corresponding job entity.
$properties['tjiid']['type'] = 'tmgmt_job_item';
// Link the author property to the corresponding user entity.
$properties['tltid']['type'] = 'tmgmt_local_task';
return $info;
}
}

View File

@@ -0,0 +1,515 @@
<?php
/**
* @file
* Provides page and forms callbacks.
*/
/**
* Simple page callback for viewing a task.
*
* @param TMGMTJob $task
* The viewed task.
*
* @return array
* A renderable array.
*/
function tmgmt_local_task_view(TMGMTLocalTask $task) {
return entity_view($task->entityType(), array($task), 'full', NULL, TRUE);
}
/**
* Entity API form the local task entity.
*/
function tmgmt_local_task_form($form, &$form_state, TMGMTLocalTask $task, $op = 'edit') {
$wrapper = entity_metadata_wrapper('tmgmt_local_task', $task);
// Set the title of the page to the label and the current status of the task.
drupal_set_title(t('@label (@status)', array('@label' => $task->label(), '@status' => $wrapper->status->label())));
// Check if the translator entity is completely new or not.
$old = empty($task->is_new) && $op != 'clone';
$form['title'] = array(
'#type' => 'textfield',
'#title' => t('Title'),
'#default_value' => $task->title,
'#access' => user_access('administer tmgmt') || user_access('administer translation tasks'),
);
$form['status'] = array(
'#type' => 'select',
'#title' => t('Status'),
'#options' => tmgmt_local_task_statuses(),
'#default_value' => $wrapper->status->value(),
'#access' => user_access('administer tmgmt') || user_access('administer translation tasks'),
);
$translators = tmgmt_local_translators($task->getJob()->source_language, array($task->getJob()->target_language));
$form['tuid'] = array(
'#title' => t('Assigned'),
'#type' => 'select',
'#options' => $translators,
'#empty_option' => t('- Select user -'),
'#default_value' => $task->tuid,
'#access' => user_access('administer tmgmt') || user_access('administer translation tasks'),
);
if ($view = views_get_view('tmgmt_local_task_items')) {
$form['items'] = array(
'#type' => 'item',
'#title' => $view->get_title(),
'#prefix' => '<div class="tmgmt-local-task-items">',
'#markup' => $view->preview('block', array($task->tltid)),
'#attributes' => array('class' => array('tmgmt-local-task-items')),
'#suffix' => '</div>',
'#weight' => 10,
);
}
// Add the buttons and action links.
$form['actions']['#type'] = 'actions';
$form['actions']['#access'] = user_access('administer tmgmt') || user_access('administer translation tasks');
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => t('Save task'),
);
if ($old) {
$form['actions']['delete'] = array(
'#type' => 'submit',
'#value' => t('Delete'),
'#redirect' => 'translate/' . $task->tltid . '/delete',
// Don't run validations, so the user can always delete the job.
'#limit_validation_errors' => array(),
);
}
return $form;
}
/**
* Submit callback for the local task form.
*/
function tmgmt_local_task_form_submit($form, &$form_state) {
$task = entity_ui_form_submit_build_entity($form, $form_state);
// If the task isn't assigned to anyone but doesn't have the unassigned status
// update it.
if ($task->tuid == 0 && !$task->isUnassigned()) {
$task->unassign();
}
$task->save();
}
/**
* Simple page callback for viewing a task.
*
* @param TMGMTLocalTaskItem $item
* The viewed task item
*
* @return array
* A renderable array.
*/
function tmgmt_local_task_item_view(TMGMTLocalTaskItem $item) {
return entity_view($item->entityType(), array($item), 'full', NULL, TRUE);
}
/**
* Assign task to current user.
*
* @param TMGMTLocalTask $task
*/
function tmgmt_local_translation_assign_to_me(TMGMTLocalTask $task) {
$task->assign($GLOBALS['user']);
$task->save();
drupal_goto('translate');
}
/**
* Form callback for translating a job item.
*/
function tmgmt_local_translation_form($form, &$form_state, TMGMTLocalTaskItem $task_item) {
$form_state['task'] = $task_item->getTask();
$form_state['task_item'] = $task_item;
$form_state['job_item'] = $job_item = $task_item->getJobItem();
$job = $job_item->getJob();
if ($job->getSetting('job_comment')) {
$form['job_comment'] = array(
'#type' => 'item',
'#title' => t('Job comment'),
'#markup' => filter_xss($job->getSetting('job_comment')),
);
}
$form['translation'] = array(
'#type' => 'container',
);
// Build the translation form.
$data = $job_item->getData();
// Need to keep the first hierarchy. So flatten must take place inside
// of the foreach loop.
$zebra = 'even';
// Reverse the order to get the correct order.
foreach (array_reverse(element_children($data)) as $key) {
$flattened = tmgmt_flatten_data($data[$key], $key);
$form['translation'][$key] = tmgmt_local_translation_form_element($flattened, $task_item, $zebra);
}
// Add the form actions as well.
$form['actions']['#type'] = 'actions';
$form['actions']['save_as_completed'] = array(
'#type' => 'submit',
'#validate' => array('tmgmt_local_translation_form_save_as_completed_validate'),
'#submit' => array('tmgmt_local_translation_form_save_submit', 'tmgmt_local_translation_form_save_as_completed_submit'),
'#value' => t('Save as completed'),
);
$form['actions']['save'] = array(
'#type' => 'submit',
'#submit' => array('tmgmt_local_translation_form_save_submit'),
'#value' => t('Save'),
);
return $form;
}
/**
* Form validate callback for save as completed submit action.
*
* Verify that all items are translated.
*/
function tmgmt_local_translation_form_save_as_completed_validate($form, &$form_state) {
// Loop over all data items and verify that there is a translation in there.
foreach ($form_state['values'] as $key => $value) {
if (is_array($value) && isset($value['translation'])) {
if (empty($value['translation'])) {
form_set_error($key . '[translation]', t('Missing translation.'));
}
}
}
}
/**
* Form submit callback for save as completed submit action.
*
* Change items to needs review state and task to completed status.
*/
function tmgmt_local_translation_form_save_as_completed_submit($form, &$form_state) {
/**
* @var TMGMTLocalTask $task.
*/
$task = $form_state['task'];
/**
* @var TMGMTLocalTaskItem $task_item.
*/
$task_item = $form_state['task_item'];
$task_item->completed();
$task_item->save();
// Mark the task as completed if all assigned job items are at needs done.
$all_done = TRUE;
foreach ($task->getItems() as $item) {
if ($item->isPending()) {
$all_done = FALSE;
break;
}
}
if ($all_done) {
$task->setStatus(TMGMT_LOCAL_TASK_STATUS_COMPLETED);
// If the task is now completed, redirect back to the overview.
$form_state['redirect'] = 'translate';
}
else {
// If there are more task items, redirect back to the task.
$uri = $task->uri();
$form_state['redirect'] = $uri['path'];
}
/**
* @var TMGMTJobItem $job_item.
*/
$job_item = $form_state['job_item'];
// Add the translations to the job item.
$job_item->addTranslatedData($task_item->getData());
}
/**
* Form submit callback for save action.
*
* Saves all items.
*/
function tmgmt_local_translation_form_save_submit($form, &$form_state) {
/**
* @var TMGMTTaskItem $task_item.
*/
$task_item = $form_state['task_item'];
// Write the translated data into the task item.
form_state_values_clean($form_state);
foreach ($form_state['values'] as $key => $value) {
if (is_array($value) && isset($value['translation'])) {
$update['#text'] = $value['translation'];
$task_item->updateData($key, $update);
}
}
$task_item->save();
$task = $form_state['task'];
$uri = $task->uri();
$form_state['redirect'] = $uri['path'];
}
/**
* Builds a translation form element.
*/
function tmgmt_local_translation_form_element($data, TMGMTLocalTaskItem $item, &$zebra) {
static $flip = array(
'even' => 'odd',
'odd' => 'even',
);
$form = array();
$job = $item->getJobItem()->getJob();
$language_list = language_list();
foreach (element_children($data) as $key) {
if (isset($data[$key]['#text']) && _tmgmt_filter_data($data[$key])) {
// The char sequence '][' confuses the form API so we need to replace it.
$target_key = str_replace('][', '|', $key);
$zebra = $flip[$zebra];
$form[$target_key] = array(
'#tree' => TRUE,
'#ajaxid' => drupal_html_id('tmgmt-local-element-' . $key),
'#theme' => 'tmgmt_local_translation_form_element',
'#parent_label' => $data[$key]['#parent_label'],
'#zebra' => $zebra,
);
$source_language = $language_list[$job->source_language];
$target_language = $language_list[$job->target_language];
$form[$target_key]['source'] = array(
'#type' => 'textarea',
'#title' => $source_language->name,
'#value' => $data[$key]['#text'],
'#disabled' => TRUE,
'#allow_focus' => TRUE,
);
$form[$target_key]['translation'] = array(
'#type' => 'textarea',
'#title' => $target_language->name,
'#default_value' => $item->getData(tmgmt_ensure_keys_array($key), '#text'),
//'#required' => TRUE,
);
$form[$target_key]['actions'] = array(
'#type' => 'container',
);
$status = $item->getData(tmgmt_ensure_keys_array($key), '#status');
$completed = $status == TMGMT_DATA_ITEM_STATE_TRANSLATED;
if ($completed) {
$form[$target_key]['actions']['reject-' . $target_key] = array(
'#type' => 'submit',
// Unicode character &#x2717 BALLOT X
'#value' => '✗',
'#attributes' => array('title' => t('Reject')),
'#name' => 'reject-' . $target_key,
'#submit' => array('tmgmt_local_translation_form_update_state_submit'),
'#ajax' => array(
'callback' => 'tmgmt_local_translation_form_update_state_ajax',
'wrapper' => $form[$target_key]['#ajaxid'],
),
'#tmgmt_local_action' => 'reject',
'#tmgmt_local_key' => str_replace('][', '|', $key),
);
}
else {
$form[$target_key]['actions']['finish-' . $target_key] = array(
'#type' => 'submit',
// Unicode character &#x2713 CHECK MARK
'#value' => '✓',
'#attributes' => array('title' => t('Finish')),
'#name' => 'finish-' . $target_key,
'#submit' => array('tmgmt_local_translation_form_update_state_submit'),
'#ajax' => array(
'callback' => 'tmgmt_local_translation_form_update_state_ajax',
'wrapper' => $form[$target_key]['#ajaxid'],
),
'#tmgmt_local_action' => 'finish',
'#tmgmt_local_key' => str_replace('][', '|', $key),
);
}
}
}
return $form;
}
/**
* Form submit callback for the translation state update button.
*/
function tmgmt_local_translation_form_update_state_submit($form, &$form_state) {
$values = $form_state['values'];
/**
* @var TMGMTLocalTaskItem $item.
*/
$item = $form_state['task_item'];
$action = $form_state['triggering_element']['#tmgmt_local_action'];
$key = $form_state['triggering_element']['#tmgmt_local_key'];
// Write the translated data into the job item.
if (isset($values[$key]) && is_array($values[$key]) && isset($values[$key]['translation'])) {
$update['#status'] = $action == 'finish' ? TMGMT_DATA_ITEM_STATE_TRANSLATED : TMGMT_DATA_ITEM_STATE_PENDING;
$update['#text'] = $values[$key]['translation'];
$item->updateData($key, $update);
$item->save();
// We need to rebuild form so we get updated action button state.
$form_state['rebuild'] = TRUE;
}
}
/**
* Ajax callback for the job item review form.
*/
function tmgmt_local_translation_form_update_state_ajax($form, &$form_state) {
$key = array_slice($form_state['triggering_element']['#array_parents'], 0, 3);
$commands = array();
$render_data = drupal_array_get_nested_value($form, $key);
$commands[] = ajax_command_replace(NULL, drupal_render($render_data));
tmgmt_ui_write_request_messages($form_state['job_item']->getJob());
$commands[] = ajax_command_html('#tmgmt-status-messages-' . strtolower($render_data['#parent_label'][0]), theme('status_messages'));
return array('#type' => 'ajax', '#commands' => $commands);
}
/**
* Form for assigning multiple tasks to translator.
*
* @param array $form
* @param array $form_state
* @param string $tasks
* Comma separated tasks ids.
*
* @return array
* Drupal form definition.
*/
function tmgmt_local_translation_assign_form($form, &$form_state, $tasks) {
$form_state['tasks'] = explode(',', $tasks);
$roles = tmgmt_local_translator_roles();
if (empty($roles)) {
drupal_set_message(t('No user role has the "provide translation services" permission. <a href="@url">Configure permissions</a> for the Local translator module.',
array('@url' => url('admin/people/permissions'))), 'warning');
}
$form['tuid'] = array(
'#title' => t('Assign to'),
'#type' => 'select',
'#empty_option' => t('Select user'),
'#options' => tmgmt_local_get_translators_for_tasks($form_state['tasks']),
'#required' => TRUE,
);
$form['actions']['#type'] = 'actions';
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => t('Assign tasks'),
);
return $form;
}
/**
* Submit handler for form for assigning multiple tasks to translator.
*/
function tmgmt_local_translation_assign_form_submit($form, &$form_state) {
$translator = user_load($form_state['values']['tuid']);
$how_many = 0;
foreach ($form_state['tasks'] as $task_id) {
$task = tmgmt_local_task_load($task_id);
if ($task) {
$task->assign($translator);
$task->save();
++$how_many;
}
}
drupal_set_message(t('Assigned @how_many to translator @translator_name.', array('@how_many' => $how_many, '@translator_name' => $translator->name)));
$form_state['redirect'] = 'manage-translate';
}
/**
* Similar to form for assigning multiple tasks to translator, but here we also
* allow no user selection.
*
* @see tmgmt_local_translation_assign_form().
*/
function tmgmt_local_translation_reassign_form($form, &$form_state, $tasks) {
$form_state['tasks'] = explode(',', $tasks);
$roles = tmgmt_local_translator_roles();
if (empty($roles)) {
drupal_set_message(t('No user role has the "provide translation services" permission. <a href="@url">Configure permissions</a> for the Local translator module.',
array('@url' => url('admin/people/permissions'))), 'warning');
}
$form['tuid'] = array(
'#title' => t('Assign to'),
'#type' => 'select',
'#empty_option' => t('Select user'),
'#options' => tmgmt_local_get_translators_for_tasks($form_state['tasks']),
);
$form['actions']['#type'] = 'actions';
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => t('Reassign tasks'),
);
return $form;
}
/**
* Submit handler for form for reassigning multiple tasks to translator.
*/
function tmgmt_local_translation_reassign_form_submit($form, &$form_state) {
if (!empty($form_state['values']['tuid'])) {
$translator = user_load($form_state['values']['tuid']);
}
else {
$translator = (object) array('uid' => 0, 'name' => t('none'));
}
$how_many = 0;
foreach ($form_state['tasks'] as $task_id) {
$task = tmgmt_local_task_load($task_id);
if ($task) {
if ($translator->uid) {
$task->assign($translator);
}
else {
$task->unassign();
}
$task->save();
++$how_many;
}
}
drupal_set_message(t('Reassigned @how_many to translator @translator_name.', array('@how_many' => $how_many, '@translator_name' => $translator->name)));
$form_state['redirect'] = 'manage-translate';
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* @file
* Provides the user translator plugin controller.
*/
/**
* Local translator plugin controller.
*/
class TMGMTLocalTranslatorPluginController extends TMGMTDefaultTranslatorPluginController {
protected $language_pairs = array();
/**
* {@inheritdoc}
*/
public function requestTranslation(TMGMTJob $job) {
$tuid = $job->getSetting('translator');
// Create local task for this job.
$local_task = tmgmt_local_task_create(array(
'uid' => $job->uid,
'tuid' => $tuid,
'tjid' => $job->tjid,
'title' => t('Task for !label', array('!label' => $job->defaultLabel())),
));
// If we have translator then switch to pending state.
if ($tuid) {
$local_task->status = TMGMT_LOCAL_TASK_STATUS_PENDING;
}
$local_task->save();
// Create task items.
foreach ($job->getItems() as $item) {
$local_task->addTaskItem($item);
}
// The translation job has been successfully submitted.
$job->submitted();
}
/**
* {@inheritdoc}
*/
public function getSupportedTargetLanguages(TMGMTTranslator $translator, $source_language) {
$languages = tmgmt_local_supported_target_languages($source_language);
if ($translator->getSetting('allow_all')) {
$languages += parent::getSupportedTargetLanguages($translator, $source_language);
}
return $languages;
}
/**
* {@inheritdoc}
*/
public function getSupportedLanguagePairs(TMGMTTranslator $translator) {
if (!empty($this->language_pairs)) {
return $this->language_pairs;
}
$roles = user_roles(TRUE, 'provide translation services');
$query = db_select('field_data_tmgmt_translation_skills', 'ts');
$query->join('users', 'u', 'u.uid = ts.entity_id AND u.status = 1');
$query->addField('ts', 'tmgmt_translation_skills_language_from', 'source_language');
$query->addField('ts', 'tmgmt_translation_skills_language_to', 'target_language');
$query->condition('ts.deleted', 0);
$query->condition('ts.entity_type', 'user');
if (!in_array(DRUPAL_AUTHENTICATED_RID, array_keys($roles))) {
$query->join('users_roles', 'ur', 'ur.uid = u.uid AND ur.rid');
$or_conditions = db_or()->condition('ur.rid', array_keys($roles))->condition('u.uid', 1);
$query->condition($or_conditions);
}
foreach ($query->execute()->fetchAll() as $item) {
$this->language_pairs[] = array(
'source_language' => $item->source_language,
'target_language' => $item->target_language,
);
}
return $this->language_pairs;
}
}

View File

@@ -0,0 +1,78 @@
<?php
/**
* @file
* Provides the user translator UI plugin controller.
*/
/**
* Local translator plugin UI controller.
*/
class TMGMTLocalTranslatorUIController extends TMGMTDefaultTranslatorUIController {
/**
* {@inheritdoc}
*/
public function checkoutSettingsForm($form, &$form_state, TMGMTJob $job) {
if ($translators = tmgmt_local_translators($job->source_language, array($job->target_language))) {
$form['translator'] = array(
'#title' => t('Select translator for this job'),
'#type' => 'select',
'#options' => array('' => t('Select user')) + $translators,
'#default_value' => $job->getSetting('translator'),
);
}
else {
$form['message'] = array(
'#markup' => t('There are no translators available.'),
);
}
return $form;
}
/**
* {@inheritdoc}
*/
public function checkoutInfo(TMGMTJob $job) {
$label = $job->getTranslator()->label();
$form['#title'] = t('@translator translation job information', array('@translator' => $label));
$form['#type'] = 'fieldset';
$tuid = $job->getSetting('translator');
if ($tuid && $translator = user_load($tuid)) {
$form['job_status'] = array(
'#type' => 'item',
'#title' => t('Job status'),
'#markup' => t('Translation job is assigned to %name.', array('%name' => entity_label('user', $translator))),
);
}
else {
$form['job_status'] = array(
'#type' => 'item',
'#title' => t('Job status'),
'#markup' => t('Translation job is not assigned to any translator.'),
);
}
if ($job->getSetting('job_comment')) {
$form['job_comment'] = array(
'#type' => 'item',
'#title' => t('Job comment'),
'#markup' => filter_xss($job->getSetting('job_comment')),
);
}
return $form;
}
public function pluginSettingsForm($form, &$form_state, TMGMTTranslator $translator, $busy = FALSE) {
$form['allow_all'] = array(
'#title' => t('Allow translations for enabled languages even if no translator has the necessary capabilities'),
'#type' => 'checkbox',
'#default_value' => $translator->getSetting('allow_all'),
);
return $form;
}
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* Renders a tables containing a group of data items belonging to the same field.
*/
function theme_tmgmt_local_translation_form($variables) {
$result = '';
// Get through all available translations.
$translation = $variables['element']['translation'];
foreach (element_children($translation) as $children_name) {
$children = $translation[$children_name];
$result .= drupal_render($children);
}
// We have rendered translation children so lets remove them.
unset($variables['element']['translation']);
// Render the rest of elements(form actions, CSRF tokens, etc.)
$result .= drupal_render_children($variables['element']);
return $result;
}
/**
* Renders a table for one data item.
*/
function theme_tmgmt_local_translation_form_element($variables) {
// Theme table which contains source, translation and action state button.
$element = $variables['element'];
$parts = explode('|', $element['#parents'][0]);
$header_title = ucfirst(str_replace('_', ' ', $parts[0]));
// Container for ajax messages.
$result = '<div id="tmgmt-status-messages-' . strtolower($element['#parent_label'][0]) . '"></div>';
$result .= theme('table', array(
'attributes' => array('id' => $element['#ajaxid'], 'class' => array($element['#zebra'])),
'header' => array(array(
'data' => $header_title,
'colspan' => 3,
)),
'rows' => array(
array(
'data' => array(
drupal_render($element['source']),
drupal_render($element['translation']),
drupal_render($element['actions']),
),
),
),
));
return $result;
}

View File

@@ -0,0 +1,11 @@
name = Translation Language capabilities
description = Provides a field that allows users to select language combinations.
package = Translation Management
core = 7.x
; 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,288 @@
<?php
/**
* Implements hook_field_info().
*/
function tmgmt_language_combination_field_info() {
$info['tmgmt_language_combination'] = array(
'label' => t('Language combination'),
'description' => t('Allows the definition of language combinations (e.g. "From english to german").'),
'default_widget' => 'tmgmt_language_combination_default',
'default_formatter' => 'tmgmt_language_combination_default',
'settings' => array(
'user_register_form' => 1,
),
);
return $info;
}
/**
* Implements hook_field_schema().
*/
function tmgmt_language_combination_field_schema($field) {
$schema = array(
'columns' => array(
'language_from' => array(
'description' => 'The langcode of the language from which the user is able to translate.',
'type' => 'varchar',
'length' => 10,
),
'language_to' => array(
'description' => 'The langcode of the language to which the user is able to translate.',
'type' => 'varchar',
'length' => 10,
),
),
'indexes' => array(
'language' => array('language_from', 'language_to'),
),
);
return $schema;
}
/**
* Implements hook_field_widget_info().
*/
function tmgmt_language_combination_field_widget_info() {
$info['tmgmt_language_combination_default'] = array(
'label' => t('Select list'),
'description' => t('Default widget for allowing users to define translation combination.'),
'field types' => array('tmgmt_language_combination'),
'behaviors' => array(
'multiple values' => FIELD_BEHAVIOR_DEFAULT,
'default value' => FIELD_BEHAVIOR_DEFAULT,
),
);
return $info;
}
/**
* Implements hook_field_widget_form().
*/
function tmgmt_language_combination_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
if (isset($form_state['list_all_languages'])) {
$languages_options = tmgmt_language_combination_languages_predefined_list();
}
else {
$languages_options = array();
foreach (language_list() as $code => $language) {
$languages_options[$code] = $language->name;
}
}
$options = array('_none' => t('- None -')) + $languages_options;
$element['language_from'] = array(
'#type' => 'select',
'#title' => t('From'),
'#options' => $options,
'#default_value' => isset($items[$delta]) ? $items[$delta]['language_from'] : '',
'#attributes' => array('class' => array('from-language')),
);
$element['language_to'] = array(
'#type' => 'select',
'#title' => t('To'),
'#options' => $options,
'#default_value' => isset($items[$delta]) ? $items[$delta]['language_to'] : '',
'#attributes' => array('class' => array('to-language')),
);
return $element;
}
/**
* Implements hook_field_validate().
*/
function tmgmt_language_combination_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
$existing = array();
foreach ($items as $delta => $item) {
$key = $item['language_from'] . ':' . $item['language_to'];
if (!tmgmt_language_combination_field_is_empty($item, 'tmgmt_language_combination')) {
if ($item['language_from'] == $item['language_to']) {
$errors[$field['field_name']][$langcode][$delta][] = array(
'error' => 'tmgmt_language_combination_equal',
'message' => t("%name: The 'from' and 'to' language fields can't have the same value.", array('%name' => $instance['label'])),
);
}
if (isset($existing[$key])) {
$errors[$field['field_name']][$langcode][$delta][] = array(
'error' => 'tmgmt_language_combination_equal',
'message' => t('%name: The language combination has to be unique.', array('%name' => $instance['label'])),
);
}
}
$existing[$key] = TRUE;
}
}
/**
* Implements hook_field_widget_error().
*/
function tmgmt_language_combination_field_widget_error($element, $error, $form, &$form_state) {
form_error($element, $error['message']);
}
/**
* Implements hook_field_is_empty().
*/
function tmgmt_language_combination_field_is_empty($item, $field) {
if (empty($item['language_from']) || empty($item['language_to']) || $item['language_from'] == '_none' || $item['language_to'] == '_none') {
return TRUE;
}
return FALSE;
}
/**
* Implements hook_field_formatter_info().
*/
function tmgmt_language_combination_field_formatter_info() {
$info['tmgmt_language_combination_default'] = array(
'label' => t('Default'),
'description' => t('Default formatter for displaying the translation combination of a user.'),
'field types' => array('tmgmt_language_combination'),
);
$info['tmgmt_language_combination_table'] = array(
'label' => t('Table'),
'description' => t('Formatter for displaying the translation combination of a user in a table.'),
'field types' => array('tmgmt_language_combination'),
);
return $info;
}
/**
* Implements hook_field_formatter_view().
*/
function tmgmt_language_combination_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
$element = array();
switch ($display['type']) {
case 'tmgmt_language_combination_default':
$element['#theme'] = 'item_list';
$element['#items'] = array();
foreach ($items as $delta => $item) {
$from = tmgmt_language_combination_language_label($item['language_from']);
$to = tmgmt_language_combination_language_label($item['language_to']);
$element['#items'][$delta]['data'] = t('From @from to @to', array('@from' => $from, '@to' => $to));
$element['#items'][$delta]['class'][] = drupal_html_class($from . '-' . $to) . '">';
}
break;
case 'tmgmt_language_combination_table':
$rows = array();
foreach ($items as $item) {
$to = tmgmt_language_combination_language_label($item['language_to']);
$from = tmgmt_language_combination_language_label($item['language_from']);
$row[] = array(
'data' => $from,
'class' => array('from-language', drupal_html_class('language-' . $from)),
);
$row[] = array(
'data' => $to,
'class' => array('to-language', drupal_html_class('language-' . $to)),
);
$rows[] = array(
'data' => $row,
'class' => array(drupal_html_class($from . '-' . $to)),
);
}
$element = array(
'#theme' => 'table',
'#header' => array(t('From'), t('To')),
'#rows' => $rows,
);
break;
}
return $element;
}
/**
* Implements hook_field_update().
*/
function tmgmt_language_combination_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) {
$languages = language_list();
$added_languages = array();
// In case the skill languages is not know to the system, install them.
foreach ($items as $item) {
if (!isset($languages[$item['language_to']]) && !isset($added_languages[$item['language_to']])) {
locale_add_language($item['language_to']);
$added_languages[$item['language_to']] = $item['language_to'];
}
if (!isset($languages[$item['language_from']]) && !isset($added_languages[$item['language_from']])) {
locale_add_language($item['language_from']);
$added_languages[$item['language_from']] = $item['language_from'];
}
}
}
/**
* Returns the label of a language.
*
* @todo Remove this once the core language label function is fixed.
*
* @param $language
* A language in ISO format.
* @return string
* The label of the language or an empty string if the language or its label
* are not defined.
*/
function tmgmt_language_combination_language_label($language) {
$languages = tmgmt_language_combination_languages_predefined_list();
if (!empty($languages[$language])) {
return $languages[$language];
}
return '';
}
/**
* Prepares a language code list for a select form item with all languages.
*/
function tmgmt_language_combination_languages_predefined_list() {
$predefined = &drupal_static(__FUNCTION__);
if (!isset($predefined)) {
include_once DRUPAL_ROOT . '/includes/iso.inc';
$predefined = _locale_get_predefined_list();
foreach ($predefined as $key => $value) {
// Include native name in output, if possible
if (count($value) > 1) {
$tname = t($value[0]);
$predefined[$key] = ($tname == $value[1]) ? $tname : "$tname ($value[1])";
}
else {
$predefined[$key] = t($value[0]);
}
}
// Add custom languages that are not part of the iso.inc definition.
$installed_languages = language_list();
foreach ($installed_languages as $lang => $info) {
if (!isset($predefined[$lang])) {
$predefined[$lang] = $info->name;
}
}
asort($predefined);
}
return $predefined;
}

View File

@@ -0,0 +1,7 @@
<?php
/*
* @file
* API documentation for the tmgmt_local module.
*/

View File

@@ -0,0 +1,37 @@
name = Local Translator
description = Allows local users to provide translation services.
package = Translation Management
core = 7.x
dependencies[] = tmgmt
dependencies[] = tmgmt_language_combination
configure = admin/config/regional/tmgmt_translator
files[] = controller/tmgmt_local.controller.task.inc
files[] = controller/tmgmt_local.controller.task_item.inc
files[] = entity/tmgmt_local.entity.task.inc
files[] = entity/tmgmt_local.entity.task_item.inc
files[] = includes/tmgmt_local.info.inc
files[] = includes/tmgmt_local.plugin.inc
files[] = includes/tmgmt_local.plugin.ui.inc
files[] = controller/tmgmt_local.ui_controller.task.inc
files[] = controller/tmgmt_local.ui_controller.task_item.inc
files[] = tmgmt_local.test
; Views integration and handlers
files[] = views/tmgmt_local.views.inc
files[] = views/handlers/tmgmt_local_task_handler_field_job_item_count.inc
files[] = views/handlers/tmgmt_local_task_handler_field_loop_count.inc
files[] = views/handlers/tmgmt_local_task_handler_field_operations.inc
files[] = views/handlers/tmgmt_local_task_handler_field_progress.inc
files[] = views/handlers/tmgmt_local_task_handler_field_item_operations.inc
files[] = views/handlers/tmgmt_local_task_handler_field_wordcount.inc
files[] = views/handlers/tmgmt_local_task_handler_filter_eligible.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,387 @@
<?php
/**
* @file
* Installation hooks for tmgmt_local module.
*/
/**
* Implements hook_schema().
*/
function tmgmt_local_schema() {
$schema['tmgmt_local_task'] = array(
'description' => 'A tmgmt local task connects translator user with assigned job items and provide additional workflow data.',
'fields' => array(
'tltid' => array(
'description' => 'The identifier of the task.',
'type' => 'serial',
'not null' => TRUE,
),
'uid' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => "User's {users}.uid for task creator.",
),
'created' => array(
'description' => 'The Unix timestamp when the task was created.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'changed' => array(
'description' => 'The Unix timestamp when the task was most recently saved.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'title' => array(
'description' => 'Task title.',
'type' => 'varchar',
'length' => 128,
),
'tuid' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'Assigned translator user {users}.uid.',
),
'tjid' => array(
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => "Translation job {tmgmt_job}.tjid that belongs to task.",
),
'status' => array(
'description' => 'The status of the task.',
'type' => 'int',
'not null' => TRUE,
),
'loop_count' => array(
'description' => 'Counter for how many times this task was returned to translator.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
'primary key' => array('tltid'),
'indexes' => array(
'tuid' => array('tuid'),
),
'foreign keys' => array(
'author' => array(
'table' => 'users',
'columns' => array('uid' => 'uid'),
),
'translator' => array(
'table' => 'users',
'columns' => array('tuid' => 'uid'),
),
'job' => array(
'table' => 'tmgmt_job',
'columns' => array('tjid' => 'tjid'),
),
),
);
$schema['tmgmt_local_task_item'] = array(
'description' => 'A tmgmt local task item contains additional workflow data for a job item.',
'fields' => array(
'tltiid' => array(
'description' => 'The identifier of the task item.',
'type' => 'serial',
'not null' => TRUE,
),
'tltid' => array(
'description' => 'Translation job task {tmgmt_local_task}.tltid that belongs to task.',
'type' => 'int',
'not null' => TRUE,
),
'tjiid' => array(
'type' => 'int',
'not null' => TRUE,
'description' => "Translation job item {tmgmt_job_item}.tjiid that belongs to task.",
),
'status' => array(
'description' => 'The status of the task.',
'type' => 'int',
'not null' => TRUE,
),
'data' => array(
'description' => 'Stores translations and translation statuses',
'type' => 'text',
'not null' => TRUE,
'size' => 'big',
'serialize' => TRUE,
),
'count_untranslated' => array(
'description' => 'Counter for all untranslated data items.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'count_translated' => array(
'description' => 'Counter for all translated data items.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'count_completed' => array(
'description' => 'Counter for all completed data items.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
'primary key' => array('tltiid'),
'indexes' => array(
'tltid' => array('tltid'),
),
'foreign keys' => array(
'task' => array(
'table' => 'tmgmt_local_task',
'columns' => array('tltid' => 'tltid'),
),
'job_item' => array(
'table' => 'tmgmt_job_item',
'columns' => array('tjiid' => 'tjiid'),
),
),
);
return $schema;
}
/**
* Adds the {tmgmt_local_task_item} table.
*/
function tmgmt_local_update_7000() {
$schema = array(
'description' => 'A tmgmt local task item contains additional workflow data for a job item.',
'fields' => array(
'tltiid' => array(
'description' => 'The identifier of the task item.',
'type' => 'serial',
'not null' => TRUE,
),
'tltid' => array(
'description' => 'Translation job task {tmgmt_local_task}.tltid that belongs to task.',
'type' => 'int',
'not null' => TRUE,
),
'tjiid' => array(
'type' => 'int',
'not null' => TRUE,
'description' => "Translation job item {tmgmt_job_item}.tjiid that belongs to task.",
),
'status' => array(
'description' => 'The status of the task.',
'type' => 'int',
'not null' => TRUE,
),
'data' => array(
'description' => 'Stores translations and translation statuses',
'type' => 'text',
'not null' => TRUE,
'size' => 'big',
'serialize' => TRUE,
),
'count_untranslated' => array(
'description' => 'Counter for all untranslated data items.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'count_translated' => array(
'description' => 'Counter for all translated data items.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'count_completed' => array(
'description' => 'Counter for all completed data items.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
'primary key' => array('tltiid'),
'indexes' => array(
'tltid' => array('tltid'),
),
'foreign keys' => array(
'task' => array(
'table' => 'tmgmt_local_task',
'columns' => array('tltid' => 'tltid'),
),
'job_item' => array(
'table' => 'tmgmt_job_item',
'columns' => array('tjiid' => 'tjiid'),
),
),
);
db_create_table('tmgmt_local_task_item', $schema);
}
/**
* Implements hook_enable().
*/
function tmgmt_local_enable() {
// If our field type is not found rebuild the cache.
if (!field_info_field_types('tmgmt_language_combination')) {
field_cache_clear();
}
// Create the language combination field if it doesn't exist yet.
if (!field_info_field('tmgmt_translation_skills')) {
$field = array(
'type' => 'tmgmt_language_combination',
'field_name' => 'tmgmt_translation_skills',
'translatable' => FALSE,
'cardinality' => FIELD_CARDINALITY_UNLIMITED,
'locked' => FALSE,
);
field_create_field($field);
}
// Attach the language skills field collection to the user entity.
if (!field_info_instance('user', 'tmgmt_translation_skills', 'user')) {
$instance = array(
'field_name' => 'tmgmt_translation_skills',
'entity_type' => 'user',
'bundle' => 'user',
'label' => t('Translation skills'),
);
field_create_instance($instance);
}
}
/**
* Implements hook_uninstall().
*/
function tmgmt_local_uninstall() {
field_delete_field('tmgmt_translation_skills');
}
/**
* Create local task items.
*/
function tmgmt_local_update_7001() {
$result = db_query('SELECT lt.tltid, ji.tjiid, ji.state, ji.data from {tmgmt_local_task} lt INNER JOIN {tmgmt_job} j ON j.tjid = lt.tjid INNER JOIN {tmgmt_job_item} ji ON ji.tjid = j.tjid');
$insert = db_insert('tmgmt_local_task_item')
->fields(array('tltid', 'tjiid', 'status', 'data'));
module_load_include('module', 'tmgmt');
foreach ($result as $row) {
$data = unserialize($row->data);
$data_count = count(array_filter(tmgmt_flatten_data($data), '_tmgmt_filter_data'));
$translation = (array)_tmgmt_local_translated_data_7001($data);
$values =array(
'tltid' => $row->tltid,
'tjiid' => $row->tjiid,
'count_untranslated' => 0,
'count_translated' => 0,
'count_translated' => 0,
'status' => 0,
'data' => serialize($translation),
);
switch ($row->state) {
case 2:
// Job item state needs review is task item status translated.
$values['status'] = 1;
$values['count_translated'] = $data_count;
break;
case 3:
// Job item state accepted is task item status completed.
$values['status'] = 3;
$values['count_completed'] = $data_count;
break;
case 1:
default:
// Job item state active is task item status untranslated.
$values['status'] = 0;
$values['count_untranslated'] = $data_count;
break;
}
$insert->values($values);
}
$insert->execute();
}
/**
* Data parsing helper function for tmgmt_local_update_7001().
*
* Copies #translation texts from the source data array to the translation
* data array.
*
* @param array $source
* The original source data array of the job item.
*
* @return array
* The filled translation data array.
*/
function _tmgmt_local_translated_data_7001($source) {
if (!empty($source['#translation']['#text'])) {
$translation['#text'] = $source['#translation']['#text'];
if ((!empty($source['#status']))) {
$translation['#status'] = $source['#status'];
}
return $translation;
}
else {
$translation = array();
foreach (element_children($source) as $key) {
if ($return = _tmgmt_local_translated_data_7001($source[$key])) {
$translation[$key] = $return;
}
}
if (!empty($translation)) {
return $translation;
}
}
}
/**
* Ensures that the tmgmt_language_combination module is enabled.
*/
function tmgmt_local_update_7002() {
// Enable the tmgmt_language_combionation module.
module_enable(array('tmgmt_language_combination'));
// Remove the tmgmt_skills module.
db_delete('system')
->condition('name', 'tmgmt_skills')
->condition('type', 'module')
->execute();
// Create the field if it doesn't exist yet.
if (!field_info_field_types('tmgmt_language_combination')) {
field_cache_clear();
}
// Create the language combination field if it doesn't exist yet.
if (!field_info_field('tmgmt_translation_skills')) {
$field = array(
'type' => 'tmgmt_language_combination',
'field_name' => 'tmgmt_translation_skills',
'translatable' => FALSE,
'cardinality' => FIELD_CARDINALITY_UNLIMITED,
'locked' => FALSE,
);
field_create_field($field);
}
// Attach the language skills field collection to the user entity.
if (!field_info_instance('user', 'tmgmt_translation_skills', 'user')) {
$instance = array(
'field_name' => 'tmgmt_translation_skills',
'entity_type' => 'user',
'bundle' => 'user',
'label' => t('Translation skills'),
);
field_create_instance($instance);
}
}

View File

@@ -0,0 +1,903 @@
<?php
/**
* @file
* Main module file for the local translation module.
*/
/**
* @defgroup tmgmt_local_task TMGMT Local Task
* @{
* Various local task API functions.
*/
/**
* Modules should return this value from hook_tmgmt_local_translation_access()
* to allow access to a node.
*/
define('TMGMT_LOCAL_TRANSLATION_ACCESS_ALLOW', 'allow');
/**
* Modules should return this value from hook_tmgmt_local_translation_access()
* to deny access to a node.
*/
define('TMGMT_LOCAL_TRANSLATION_ACCESS_DENY', 'deny');
/**
* Modules should return this value from hook_tmgmt_local_translation_access()
* to not affect node access.
*/
define('TMGMT_LOCAL_TRANSLATION_ACCESS_IGNORE', NULL);
/**
* Translation task is not assigned to translator.
*/
define('TMGMT_LOCAL_TASK_STATUS_UNASSIGNED', 0);
/**
* Translation task is pending.
*/
define('TMGMT_LOCAL_TASK_STATUS_PENDING', 1);
/**
* Translation task is completed (all job items are translated).
*/
define('TMGMT_LOCAL_TASK_STATUS_COMPLETED', 2);
/**
* Translation task is rejected (at least some job items are rejected).
*/
define('TMGMT_LOCAL_TASK_STATUS_REJECTED', 3);
/**
* Translation task is closed.
*/
define('TMGMT_LOCAL_TASK_STATUS_CLOSED', 4);
/**
* Translation task item is untranslated.
*/
define('TMGMT_LOCAL_TASK_ITEM_STATUS_PENDING', 0);
/**
* Translation task item is translated and pending review of the job item.
*/
define('TMGMT_LOCAL_TASK_ITEM_STATUS_COMPLETED', 1);
/**
* Translation job item has been rejected and the task needs to be updated.
*/
define('TMGMT_LOCAL_TASK_ITEM_STATUS_REJECTED', 2);
/**
* The translation task item has been completed and closed.
*/
define('TMGMT_LOCAL_TASK_ITEM_STATUS_CLOSED', 3);
/**
* Untranslated translation data item.
*/
define('TMGMT_DATA_ITEM_STATE_UNTRANSLATED', 0);
/**
* @} End of "tmgmt_local_task".
*/
/**
* Implements hook_entity_info().
*/
function tmgmt_local_entity_info() {
$info = array(
'tmgmt_local_task' => array(
'label' => t('Translation Task'),
'module' => 'tmgmt_local',
'entity class' => 'TMGMTLocalTask',
'controller class' => 'TMGMTLocalTaskController',
'metadata controller class' => 'TMGMTLocalTaskMetadataController',
'views controller class' => 'TMGMTLocalTaskViewsController',
'base table' => 'tmgmt_local_task',
'uri callback' => 'entity_class_uri',
'label callback' => 'entity_class_label',
'access callback' => 'tmgmt_local_task_access',
'entity keys' => array(
'id' => 'tltid',
),
'admin ui' => array(
'controller class' => 'TMGMTLocalTaskUIController',
'path' => 'translate',
),
),
'tmgmt_local_task_item' => array(
'label' => t('Translation Task Item'),
'module' => 'tmgmt_local',
'entity class' => 'TMGMTLocalTaskItem',
'controller class' => 'TMGMTLocalTaskItemController',
'metadata controller class' => 'TMGMTLocalTaskItemMetadataController',
'views controller class' => 'TMGMTLocalTaskItemViewsController',
'base table' => 'tmgmt_local_task_item',
'uri callback' => 'entity_class_uri',
'label callback' => 'entity_class_label',
'access callback' => 'tmgmt_local_task_item_access',
'entity keys' => array(
'id' => 'tltiid',
),
'admin ui' => array(
'controller class' => 'TMGMTLocalTaskItemUIController',
'path' => 'translate',
),
),
);
return $info;
}
/**
* Implements hook_theme().
*/
function tmgmt_local_theme() {
return array(
'tmgmt_local_translation_form' => array(
'render element' => 'element',
'file' => 'includes/tmgmt_local.theme.inc',
),
'tmgmt_local_translation_form_element' => array(
'render element' => 'element',
'file' => 'includes/tmgmt_local.theme.inc',
),
// @todo - not implemented.
'tmgmt_local_translation_form_element_status' => array(
'render element' => 'status',
'file' => 'includes/tmgmt_local.theme.inc',
),
);
}
/**
* Implements hook_menu().
*/
function tmgmt_local_menu() {
$items['translate/%tmgmt_local_task/assign-to-me'] = array(
'title' => 'Assign translation task to me',
'description' => 'Assign translation task to current translator user.',
'page callback' => 'tmgmt_local_translation_assign_to_me',
'page arguments' => array(1),
'access callback' => 'tmgmt_local_translation_access',
'access arguments' => array(1),
'file' => 'includes/tmgmt_local.pages.inc',
);
$items['manage-translate/assign-tasks'] = array(
'title' => 'Assign translation task',
'description' => 'Assign translation tasks to specific translator.',
'page callback' => 'drupal_get_form',
'page arguments' => array('tmgmt_local_translation_assign_form', 2),
'access arguments' => array('administer translation tasks'),
'file' => 'includes/tmgmt_local.pages.inc',
'type' => MENU_CALLBACK,
);
$items['manage-translate/reassign-tasks'] = array(
'title' => 'Reassign translation task',
'description' => 'Ressign translation tasks to specific translator.',
'page callback' => 'drupal_get_form',
'page arguments' => array('tmgmt_local_translation_reassign_form', 2),
'access arguments' => array('administer translation tasks'),
'file' => 'includes/tmgmt_local.pages.inc',
'type' => MENU_CALLBACK,
);
return $items;
}
/**
* Implements hook_permission().
*/
function tmgmt_local_permission() {
return array(
'provide translation services' => array(
'title' => t('Provide translation services'),
'descriptions' => t('Root permission for translation access: Users with this permission are eligible to be granted translation access to a translation task.'),
),
'administer translation tasks' => array(
'title' => t('Administer translation tasks'),
'description' => t('Administer translation tasks.'),
),
);
}
/**
* Implements hook_views_api().
*/
function tmgmt_local_views_api() {
return array(
'api' => 3.0,
'path' => drupal_get_path('module', 'tmgmt_local') . '/views',
);
}
/**
* Implements hook_default_views().
*/
function tmgmt_local_views_default_views() {
return _tmgmt_load_exports('tmgmt_local', 'views', 'view.inc', 'view');
}
/**
* Implements hook_tmgmt_translator_plugin_info().
*/
function tmgmt_local_tmgmt_translator_plugin_info() {
$info['local'] = array(
'label' => t('Local Translator'),
'description' => t('Allows local users to process translation jobs.'),
'plugin controller class' => 'TMGMTLocalTranslatorPluginController',
'ui controller class' => 'TMGMTLocalTranslatorUIController',
'map remote languages' => FALSE,
);
return $info;
}
/**
* @addtogroup tmgmt_local
* @{
*/
/**
* Determine whether the current user is allowed to translate a given
* translation task.
*
* @param $task
* The translation task to be translated.
* @param $account
* (Optional) A user object representing the user that is trying to obtain
* translation access. Determines access for a user other than the current
* user.
* @return bool
* TRUE if the user is allowed to translate the given translation job, FALSE
* otherwise.
*/
function tmgmt_local_translation_access(TMGMTLocalTask $task, $account = NULL) {
$job = $task->getJob();
if (!$job || !$job->isActive()) {
return FALSE;
}
$rights = &drupal_static(__FUNCTION__);
// If no user object is supplied, the access check is for the current user.
if (empty($account)) {
$account = $GLOBALS['user'];
}
// If we've already checked access for this job and user, return from cache.
if (isset($rights[$account->uid][$job->tjid])) {
return $rights[$account->uid][$job->tjid];
}
// We grant access to the translation if both of the following conditions are
// met:
// - User is assigned as a translator to the given task.
// - User has 'provide translation services' permission.
// - No modules say to deny access.
// - At least one module says to grant access.
// - User has translation capabilities for this task.
if (!user_access('provide translation services')) {
$rights[$account->uid][$job->tjid] = FALSE;
return FALSE;
}
if ($task->tuid == $account->uid) {
$rights[$account->uid][$job->tjid] = TRUE;
return TRUE;
}
$access = module_invoke_all('tmgmt_local_translation_access', $job, $account);
if (in_array(TMGMT_LOCAL_TRANSLATION_ACCESS_DENY, $access, TRUE)) {
$rights[$account->uid][$job->tjid] = FALSE;
return FALSE;
}
elseif (in_array(TMGMT_LOCAL_TRANSLATION_ACCESS_ALLOW, $access, TRUE)) {
$rights[$account->uid][$job->tjid] = TRUE;
return TRUE;
}
// Lastly, check for the translation capabilities.
$target_languages = tmgmt_local_supported_target_languages($job->source_language, array($account->uid));
$rights[$account->uid][$job->tjid] = in_array($job->target_language, $target_languages);
return $rights[$account->uid][$job->tjid];
}
/**
* Helper function for clearing the languages cache of all local translators.
*
* Can be used in oder to clear the cache for supported target languages after
* the translation capabilities of an local have changed.
*/
function tmgmt_local_clear_languages_cache() {
$query = new EntityFieldQuery();
$query->entityCondition('entity_type', 'tmgmt_translator');
$query->propertyCondition('plugin', 'local');
$result = $query->execute();
if ($result) {
foreach (tmgmt_translator_load_multiple(array_keys($result['tmgmt_translator'])) as $translator) {
$translator->clearLanguageCache();
}
}
}
/**
* Loads a local translation task entity.
*
* @return TMGMTLocalTask
*/
function tmgmt_local_task_load($tltid) {
return entity_load_single('tmgmt_local_task', $tltid);
}
/**
* Loads local translation tasks entity.
*/
function tmgmt_local_task_load_multiple(array $tltids = array(), $conditions = array()) {
return entity_load('tmgmt_local_task', $tltids, $conditions);
}
/**
* Loads a local translation task items entity.
*
* @return TMGMTLocalTaskItem
*/
function tmgmt_local_task_item_load($tltiid) {
return entity_load_single('tmgmt_local_task_item', $tltiid);
}
/**
* Loads local translation task items entity.
*/
function tmgmt_local_task_item_load_multiple(array $tltiids = array(), $conditions = array()) {
return entity_load('tmgmt_local_task_item', $tltiids, $conditions);
}
/**
* Creates a translation task entity.
*
* @param $values
* (Optional) An array of additional entity values.
*
* @return TMGMTLocalTask
* The local translation task entity.
*/
function tmgmt_local_task_create(array $values = array()) {
return entity_create('tmgmt_local_task', $values);
}
/**
* Deletes multiple local tasks entities.
*
* @param $tltids
* An array of local tasks IDs.
*/
function tmgmt_local_task_delete_multiple(array $tltids) {
entity_get_controller('tmgmt_local_task')->delete($tltids);
}
/**
* Access callback for the local task entity.
*
* @param $op
* The operation being performed.
* @param $item
* (Optional) A TMGMTLocalTask entity to check access for. If no entity is
* given, it will be determined whether access is allowed for all entities.
* @param $account
* (Optional) The user to check for. Leave it to NULL to check for the global
* user.
*
* @return boolean
* TRUE if access is allowed, FALSE otherwise.
*/
function tmgmt_local_task_access($op, $task = NULL, $account = NULL) {
if (user_access('administer tmgmt', $account) || user_access('administer translation tasks', $account)) {
// Administrators can do everything.
return TRUE;
}
if (!$account) {
global $user;
$account = $user;
}
// @todo - probably need refinement when we introduce more module permissions.
switch ($op) {
case 'view':
case 'update':
return user_access('provide translation services', $account);
break;
case 'unassign':
return !empty($task->tuid) && $task->tuid == $account->uid && user_access('provide translation services', $account);
}
}
/**
* Access callback for the local task item entity.
*
* @param $op
* The operation being performed.
* @param $item
* (Optional) A TMGMTLocalTaskItem entity to check access for. If no entity is
* given, it will be determined whether access is allowed for all entities.
* @param $account
* (Optional) The user to check for. Leave it to NULL to check for the global
* user.
*
* @return boolean
* TRUE if access is allowed, FALSE otherwise.
*/
function tmgmt_local_task_item_access($op, TMGMTLocalTaskItem $item = NULL, $account = NULL) {
$task = NULL;
if ($item) {
$task = $item->getTask();
}
return entity_access($op, 'tmgmt_local_task', $task, $account);
}
/**
* Loads an array with the word and status statistics of a task.
*
* @param $tltids
* An array of local task ids.
*
* @return
* An array of objects with the keys word_count, count_pending,
* count_accepted, count_translated and loop_count.
*/
function tmgmt_local_task_statistics_load(array $tltids) {
$statistics = &drupal_static(__FUNCTION__, array());
// First try to get the values from the cache.
$return = array();
$tltids_to_load = array();
foreach ($tltids as $tltid) {
if (isset($statistics[$tltid])) {
// Info exists in cache, get it from there.
$return[$tltid] = $statistics[$tltid];
}
else {
// Info doesn't exist in cache, add job to the list that needs to be
// fetched.
$tltids_to_load[] = $tltid;
}
}
// If there are remaining jobs, build a query to fetch them.
if (!empty($tltids_to_load)) {
// Build the query to fetch the statistics.
$query = db_select('tmgmt_local_task_item', 'tlti');
$query->join('tmgmt_local_task', 'tlt', 'tlt.tltid = tlti.tltid');
$query->join('tmgmt_job_item', 'tji', 'tji.tjiid = tlti.tjiid');
$query->fields('tlt', array('tltid'));
$query->addExpression('SUM(tji.word_count)', 'word_count');
$query->addExpression('SUM(tlti.count_untranslated)', 'count_untranslated');
$query->addExpression('SUM(tlti.count_translated)', 'count_translated');
$query->addExpression('SUM(tlti.count_completed)', 'count_completed');
$result = $query->groupBy('tlt.tltid')
->condition('tlt.tltid', $tltids_to_load)
->execute();
foreach ($result as $row) {
$return[$row->tltid] = $statistics[$row->tltid] = $row;
}
}
return $return;
}
/**
* Returns a specific statistic of a task.
*
* @param $task
* The translation task entity.
* @param $key
* One of word_count, loop_count, count_pending, count_accepted and
* count_translated.
*
* @return
* The requested information as an integer.
*/
function tmgmt_local_task_statistic(TMGMTLocalTask $task, $key) {
$statistics = tmgmt_local_task_statistics_load(array($task->tltid));
if (isset($statistics[$task->tltid]->$key)) {
return $statistics[$task->tltid]->$key;
}
return 0;
}
/**
* Retrieve a labeled list of all available statuses.
*
* @return array
* A list of all available statuses.
*/
function tmgmt_local_task_statuses() {
return $statuses = array(
TMGMT_LOCAL_TASK_STATUS_UNASSIGNED => t('Unassigned'),
TMGMT_LOCAL_TASK_STATUS_PENDING => t('Pending'),
TMGMT_LOCAL_TASK_STATUS_COMPLETED => t('Completed'),
TMGMT_LOCAL_TASK_STATUS_REJECTED => t('Rejected'),
TMGMT_LOCAL_TASK_STATUS_CLOSED => t('Closed'),
);
}
/**
* Retrieve a labeled list of all available statuses for task items.
*
* @return array
* A list of all available statuses.
*/
function tmgmt_local_task_item_statuses() {
return $statuses = array(
TMGMT_LOCAL_TASK_ITEM_STATUS_PENDING => t('Untranslated'),
TMGMT_LOCAL_TASK_ITEM_STATUS_COMPLETED => t('Translated'),
TMGMT_LOCAL_TASK_ITEM_STATUS_REJECTED => t('Rejected'),
TMGMT_LOCAL_TASK_ITEM_STATUS_CLOSED => t('Completed'),
);
}
/**
* Gets all involved language pairs for given tasks.
*
* @param array $tasks
* Array of tasks ids.
*
* @return array
* Array of involved languages.
*/
function tmgmt_local_tasks_languages($tasks) {
$query = db_select('tmgmt_local_task', 't');
$query->condition('tltid', $tasks);
$query->join('tmgmt_job', 'j', 't.tjid = j.tjid');
$query->fields('j', array('source_language', 'target_language'));
$query->groupBy('target_language');
$result = $query->execute();
$languages = array();
foreach ($result as $row) {
if (empty($languages[$row->source_language]) || !in_array($row->target_language, $languages[$row->source_language])) {
$languages[$row->source_language][] = $row->target_language;
}
}
return $languages;
}
/**
* Gets translators able to translate all given tasks.
*
* @param array $tasks
* Array of tasks ids.
*
* @return array
* List of uid => name values.
*/
function tmgmt_local_get_translators_for_tasks($tasks) {
$translators = array();
foreach (tmgmt_local_tasks_languages($tasks) as $source_language => $target_languages) {
$translators[] = tmgmt_local_translators($source_language, $target_languages);
}
if (count($translators) > 1) {
return call_user_func_array('array_intersect', $translators);
}
elseif (count($translators) == 1) {
return array_shift($translators);
}
return array();
}
/**
* @} End of "addtogroup tmgmt_local_task".
*/
/**
* Implements hook_tmgmt_job_item_update().
*
* @todo: Move this to the translator plugin API.
*/
function tmgmt_local_tmgmt_job_item_update(TMGMTJobItem $job_item) {
if ($job_item->isAccepted() && !$job_item->original->isAccepted()) {
$tltiid = db_query('SELECT tltiid FROM {tmgmt_local_task_item} ti INNER JOIN {tmgmt_local_task} t ON ti.tltid = t.tltid WHERE t.tjid = :tjid AND ti.tjiid = :tjiid', array(':tjid' => $job_item->tjid, ':tjiid' => $job_item->tjiid))->fetchField();
if ($tltiid) {
$task_item = tmgmt_local_task_item_load($tltiid);
$task_item->closed();
$task_item->save();
// Check if the task can be marked as closed as well.
$query = new EntityFieldQuery();
$unclosed_tasks = $query->entityCondition('entity_type', 'tmgmt_local_task_item')
->propertyCondition('tltid', $task_item->tltid)
->propertyCondition('status', TMGMT_LOCAL_TASK_ITEM_STATUS_CLOSED, '<>')
->count()
->execute();
if ($unclosed_tasks == 0) {
$task = $task_item->getTask();
$task->setStatus(TMGMT_LOCAL_TASK_STATUS_CLOSED);
$task->save();
}
}
}
}
/**
* Implements hook_tmgmt_job_delete().
*/
function tmgmt_local_tmgmt_job_delete(TMGMTJob $job) {
$query = new EntityFieldQuery();
$result = $query
->entityCondition('entity_type', 'tmgmt_local_task')
->propertyCondition('tjid', $job->tjid)
->execute();
if (!empty($result['tmgmt_local_task'])) {
entity_delete_multiple('tmgmt_local_task', array_keys($result['tmgmt_local_task']));
}
}
/**
* Implements hook_field_access().
*/
function tmgmt_local_field_access($op, $field, $entity_type, $entity = NULL, $account = NULL) {
if ($field['field_name'] == 'tmgmt_translation_skills' && $entity_type == 'user') {
$account = !empty($account) ? $account : $GLOBALS['user'];
// If the field is just being viewed, grant access.
if ($op == 'view') {
return TRUE;
}
// User can provide transl. services and is dealing with own account.
if (!empty($entity) && $entity->uid == $account->uid) {
return user_access('provide translation services', $entity);
}
// Administrators are allowed to deal with others only.
if (user_access('administer tmgmt', $account)) {
return TRUE;
}
return FALSE;
}
}
/**
* Implements hook_field_attach_insert().
*/
function tmgmt_local_field_attach_insert($entity_type, $entity) {
if ($entity_type != 'user' || !array_intersect_key(user_roles(TRUE, 'provide translation services'), $entity->roles)) {
return;
}
tmgmt_local_clear_languages_cache();
}
/**
* Implements hook_field_attach_update().
*/
function tmgmt_local_field_attach_update($entity_type, $entity) {
if ($entity_type != 'user' || !array_intersect_key(user_roles(TRUE, 'provide translation services'), $entity->roles)) {
return;
}
tmgmt_local_clear_languages_cache();
}
/**
* Implements hook_field_attach_delete().
*/
function tmgmt_local_field_attach_delete($entity_type, $entity) {
if ($entity_type != 'user' || !array_intersect_key(user_roles(TRUE, 'provide translation services'), $entity->roles)) {
return;
}
tmgmt_local_clear_languages_cache();
}
/**
* Gets list of language pairs.
*
* @param string $source_language
* Source language code for which to limit the selection.
* @param array $uids
* List of user ids for whom to get the language pairs.
*
* @return array
* List of language pairs where a pair is defined by associative array of
* source_language and target_language keys.
*/
function tmgmt_local_supported_language_pairs($source_language = NULL, $uids = array()) {
$language_pairs = &drupal_static(__FUNCTION__);
$cache_key = $source_language . '_' . implode('_', $uids);
if (isset($language_pairs[$cache_key])) {
return $language_pairs[$cache_key];
}
$language_pairs[$cache_key] = array();
foreach (tmgmt_local_capabilities($source_language, NULL, $uids) as $row) {
// Prevent duplicates.
$pair_key = $row->tmgmt_translation_skills_language_from . '__' . $row->tmgmt_translation_skills_language_to;
$language_pairs[$cache_key][$pair_key] = array(
'source_language' => $row->tmgmt_translation_skills_language_from,
'target_language' => $row->tmgmt_translation_skills_language_to,
);
}
return $language_pairs[$cache_key];
}
/**
* Gets supported target languages.
*
* @param string $source_language
* Source language for which to get target languages.
* @param array $uids
* List of user ids for whom to get the target languages.
*
* @return array
* List of target languages where code is the key as well as the value.
*/
function tmgmt_local_supported_target_languages($source_language, $uids = array()) {
$pairs = tmgmt_local_supported_language_pairs($source_language, $uids);
$supported_languages = array();
foreach ($pairs as $pair) {
$supported_languages[$pair['target_language']] = $pair['target_language'];
}
return $supported_languages;
}
/**
* Gets local translator for given language combination.
*
* @param string $source_language
* (optional) Source language to limit on.
* @param array $target_languages
* (optional) List of target languages to limit to.
*
* @return array
* Array of uid => name translators or empty array if there are no translator
* users.
*/
function tmgmt_local_translators($source_language = NULL, array $target_languages = array()) {
$translators = &drupal_static(__FUNCTION__);
$key = $source_language . '_' . implode('_', $target_languages);
if (isset($translators[$key])) {
return $translators[$key];
}
// Get all capabilities keyed by uids for given source language.
$translators_capabilities = array();
foreach (tmgmt_local_capabilities($source_language) as $row) {
$translators_capabilities[$row->uid][] = $row->tmgmt_translation_skills_language_to;
}
// Filter out translator uids who's capabilities are not sufficient for given
// target languages.
$translators_uids = array();
foreach ($translators_capabilities as $uid => $translator_capabilities) {
// In case provided target languages exceed users capabilities exclude.
if (!empty($target_languages) && count(array_diff($target_languages, $translator_capabilities)) > 0) {
continue;
}
$translators_uids[] = $uid;
}
// Finally build the translators list.
$translators[$key] = array();
if (!empty($translators_uids)) {
foreach (user_load_multiple($translators_uids) as $account) {
$translators[$key][$account->uid] = entity_label('user', $account);
}
}
return $translators[$key];
}
/**
* Gets language capabilities.
*
* @param string $source_language
* (optional) Limit the source language.
* @param string $target_language
* (optional) Limit the target language.
* @param array $uids
* (optional) Limit to specific users.
*
* @return array
* Array of language capabilities with following data:
* - tmgmt_translation_skills_language_from
* - tmgmt_translation_skills_language_to
* - uid
* - name
* - mail
*/
function tmgmt_local_capabilities($source_language = NULL, $target_language = NULL, $uids = array()) {
$roles = tmgmt_local_translator_roles();
// If there are no roles that have the required permission, return an empty
// array.
if (empty($roles)) {
return array();
}
$query = db_select('field_data_tmgmt_translation_skills', 'ts')
->fields('ts', array('tmgmt_translation_skills_language_from', 'tmgmt_translation_skills_language_to'))
->condition('ts.deleted', 0)
->condition('ts.entity_type', 'user');
if ($source_language) {
$query->condition('ts.tmgmt_translation_skills_language_from', $source_language);
}
if ($target_language) {
$query->condition('ts.tmgmt_translation_skills_language_to', $target_language);
}
// Join only active users.
$query->innerJoin('users', 'u', 'u.uid = ts.entity_id AND u.status = 1');
$query->fields('u', array('uid', 'name', 'mail'));
if (!empty($uids)) {
$query->condition('u.uid', $uids);
}
// If the authenticated user role has the required permission we do not have
// to do the role check.
if (!in_array('authenticated user', $roles)) {
$query->leftJoin('users_roles', 'ur', "ur.uid = u.uid AND ur.rid IN (:roles)", array(':roles' => array_keys($roles)));
}
// Add a tag so other modules can alter this query at will.
$query->addTag('tmgmt_translation_combination');
return $query->execute()->fetchAll();
}
/**
* Get roles with 'provide translation services' permissions.
*
* @return array
* An associative array with the role id as the key and the role name as
* value.
*/
function tmgmt_local_translator_roles() {
return user_roles(TRUE, 'provide translation services');
}
/**
* Implements hook_rules_action_info_alter().
*/
function tmgmt_local_rules_action_info_alter(&$actions) {
if (isset($actions['component_rules_tmgmt_local_task_assign_to_me'])) {
$actions['component_rules_tmgmt_local_task_assign_to_me']['access callback'] = 'tmgmt_local_rules_component_access';
}
if (isset($actions['component_rules_tmgmt_local_task_assign'])) {
$actions['component_rules_tmgmt_local_task_assign']['access callback'] = 'tmgmt_local_rules_component_access';
}
}
/**
* Access callback to translation tasks rules component actions.
*/
function tmgmt_local_rules_component_access($type, $name) {
switch ($name) {
case 'component_rules_tmgmt_local_task_assign_to_me':
return user_access('provide translation services');
case 'component_rules_tmgmt_local_task_assign';
return user_access('administer translation tasks');
}
}

View File

@@ -0,0 +1,65 @@
<?php
/**
* @file
* Rules integration.
*/
/**
* Implements hook_rules_action_info().
*/
function tmgmt_local_rules_action_info() {
$info['tmgmt_local_rules_task_assign'] = array(
'label' => t('Assign translation task'),
'group' => t('Translation Management'),
'parameter' => array(
'task' => array(
'type' => 'tmgmt_local_task',
'label' => t('Translation task'),
'description' => t('The translation task that should be assigned to the configured user.'),
),
'user' => array(
'type' => 'user',
'label' => t('user'),
'description' => t('The assigned user.'),
),
),
);
$info['tmgmt_local_rules_task_unassign'] = array(
'label' => t('Unassign translation task'),
'group' => t('Translation Management'),
'parameter' => array(
'task' => array(
'type' => 'tmgmt_local_task',
'label' => t('Translation task'),
'description' => t('The translation task which will be unassigned.'),
),
),
'access callback' => 'tmgmt_local_rules_task_access',
);
return $info;
}
/**
* Rules callback to assign a translation task to translator.
*/
function tmgmt_local_rules_task_assign(TMGMTLocalTask $task, stdClass $translator) {
$task->assign($translator);
$task->save();
}
/**
* Rules callback to unassign a translation task.
*/
function tmgmt_local_rules_task_unassign(TMGMTLocalTask $task) {
$task->unassign();
$task->save();
}
/**
* Access callback to the unassign task rules action.
*/
function tmgmt_local_rules_task_access() {
return user_access('administer translation tasks') || user_access('provide translation services');
}

View File

@@ -0,0 +1,57 @@
<?php
/*
* @file
* Contains default rules.
*/
/**
* Implements hook_default_rules_configuration().
*/
function tmgmt_local_default_rules_configuration() {
$data = '{ "rules_tmgmt_local_task_assign_to_me" : {
"LABEL" : "Assign Translation Task To Me",
"PLUGIN" : "rule",
"REQUIRES" : [ "tmgmt_local" ],
"USES VARIABLES" : { "task" : { "label" : "Task", "type" : "tmgmt_local_task" } },
"DO" : [
{ "tmgmt_local_rules_task_assign" : { "task" : [ "task" ], "user" : [ "site:current-user" ] } }
]
}
}';
$rule = rules_import($data);
$configs[$rule->name] = $rule;
$data = ' { "rules_tmgmt_local_task_assign" : {
"LABEL" : "Assign Translation Task",
"PLUGIN" : "rule",
"REQUIRES" : [ "tmgmt" ],
"USES VARIABLES" : {
"task" : { "label" : "Task", "type" : "tmgmt_local_task" },
"translator" : { "label" : "Translator", "type" : "user" }
},
"DO" : [
{ "tmgmt_local_rules_task_assign" : {
"task" : [ "task" ],
"translator" : [ "translator" ]
}
}
]
}
}';
$rule = rules_import($data);
$configs[$rule->name] = $rule;
$data = ' { "rules_tmgmt_local_task_unassign" : {
"LABEL" : "Unassign Translation Task",
"PLUGIN" : "rule",
"REQUIRES" : [ "tmgmt_local" ],
"USES VARIABLES" : { "task" : { "label" : "Task", "type" : "tmgmt_local_task" } },
"DO" : [ { "tmgmt_local_rules_task_unassign" : { "task" : [ "task" ] } } ]
}
}';
$rule = rules_import($data);
$configs[$rule->name] = $rule;
return $configs;
}

View File

@@ -0,0 +1,614 @@
<?php
/**
* @file
* Test cases for the local translator module.
*/
/**
* Basic tests for the local translator.
*/
class TMGMTLocalTestCase extends TMGMTBaseTestCase {
/**
* Translator user.
*
* @var object
*/
protected $local_translator;
protected $local_translator_permissions = array(
'provide translation services',
);
static function getInfo() {
return array(
'name' => 'Local Translator tests',
'description' => 'Tests the local translator plugin integration.',
'group' => 'Translation Management',
);
}
function setUp() {
parent::setUp(array('tmgmt_language_combination', 'tmgmt_local', 'tmgmt_ui'));
$this->loginAsAdmin();
$this->setEnvironment('de');
}
function testTranslatorSkillsForTasks() {
$this->setEnvironment('fr');
$translator1 = $this->drupalCreateUser($this->local_translator_permissions);
$this->drupalLogin($translator1);
$edit = array(
'tmgmt_translation_skills[und][0][language_from]' => 'en',
'tmgmt_translation_skills[und][0][language_to]' => 'de',
);
$this->drupalPost('user/' . $translator1->uid . '/edit', $edit, t('Save'));
$translator2 = $this->drupalCreateUser($this->local_translator_permissions);
$this->drupalLogin($translator2);
$edit = array(
'tmgmt_translation_skills[und][0][language_from]' => 'en',
'tmgmt_translation_skills[und][0][language_to]' => 'de',
);
$this->drupalPost('user/' . $translator2->uid . '/edit', $edit, t('Save'));
$edit = array(
'tmgmt_translation_skills[und][1][language_from]' => 'de',
'tmgmt_translation_skills[und][1][language_to]' => 'en',
);
$this->drupalPost('user/' . $translator2->uid . '/edit', $edit, t('Save'));
$translator3 = $this->drupalCreateUser($this->local_translator_permissions);
$this->drupalLogin($translator3);
$edit = array(
'tmgmt_translation_skills[und][0][language_from]' => 'en',
'tmgmt_translation_skills[und][0][language_to]' => 'de',
);
$this->drupalPost('user/' . $translator3->uid . '/edit', $edit, t('Save'));
$edit = array(
'tmgmt_translation_skills[und][1][language_from]' => 'de',
'tmgmt_translation_skills[und][1][language_to]' => 'en',
);
$this->drupalPost('user/' . $translator3->uid . '/edit', $edit, t('Save'));
$edit = array(
'tmgmt_translation_skills[und][2][language_from]' => 'en',
'tmgmt_translation_skills[und][2][language_to]' => 'fr',
);
$this->drupalPost('user/' . $translator3->uid . '/edit', $edit, t('Save'));
$job1 = $this->createJob('en', 'de');
$job2 = $this->createJob('de', 'en');
$job3 = $this->createJob('en', 'fr');
$local_task1 = tmgmt_local_task_create(array(
'uid' => $job1->uid,
'tjid' => $job1->tjid,
'title' => 'Task 1',
));
$local_task1->save();
$local_task2 = tmgmt_local_task_create(array(
'uid' => $job2->uid,
'tjid' => $job2->tjid,
'title' => 'Task 2',
));
$local_task2->save();
$local_task3 = tmgmt_local_task_create(array(
'uid' => $job3->uid,
'tjid' => $job3->tjid,
'title' => 'Task 3',
));
$local_task3->save();
// Test languages involved in tasks.
$this->assertEqual(
tmgmt_local_tasks_languages(array($local_task1->tltid, $local_task2->tltid, $local_task3->tltid)),
array(
'en' => array('de', 'fr'),
'de' => array('en'),
)
);
// Test available translators for task en - de.
$this->assertEqual(
tmgmt_local_get_translators_for_tasks(array($local_task1->tltid)),
array(
$translator1->uid => entity_label('user', $translator1),
$translator2->uid => entity_label('user', $translator2),
$translator3->uid => entity_label('user', $translator3),
)
);
// Test available translators for tasks en - de, de - en.
$this->assertEqual(
tmgmt_local_get_translators_for_tasks(array($local_task1->tltid, $local_task2->tltid)),
array(
$translator2->uid => entity_label('user', $translator2),
$translator3->uid => entity_label('user', $translator3),
)
);
// Test available translators for tasks en - de, de - en, en - fr.
$this->assertEqual(
tmgmt_local_get_translators_for_tasks(array($local_task1->tltid, $local_task2->tltid, $local_task3->tltid)),
array(
$translator3->uid => entity_label('user', $translator3),
)
);
}
/**
* Test the basic translation workflow
*/
function testBasicWorkflow() {
$translator = tmgmt_translator_load('local');
// Create a job and request a local translation.
$this->loginAsTranslator();
$job = $this->createJob();
$job->translator = $translator->name;
$job->settings['job_comment'] = $job_comment = 'Dummy job comment';
$job->addItem('test_source', 'test', '1');
$job->addItem('test_source', 'test', '2');
$job->save();
$uri = $job->uri();
// Make sure that the checkout page works as expected when there are no
// roles.
$this->drupalGet($uri['path']);
$this->assertText(t('@translator can not translate from @source to @target.', array('@translator' => 'Local Translator (auto created)', '@source' => 'English', '@target' => 'German')));
$this->local_translator = $this->drupalCreateUser($this->local_translator_permissions);
// The same when there is a single role.
$this->drupalGet($uri['path']);
$this->assertText(t('@translator can not translate from @source to @target.', array('@translator' => 'Local Translator (auto created)', '@source' => 'English', '@target' => 'German')));
// Create another local translator with the required capabilities.
$other_translator_same = $this->drupalCreateUser($this->local_translator_permissions);
// And test again with two roles but still no capabilities.
$this->drupalGet($uri['path']);
$this->assertText(t('@translator can not translate from @source to @target.', array('@translator' => 'Local Translator (auto created)', '@source' => 'English', '@target' => 'German')));
$this->drupalLogin($other_translator_same);
// Configure language capabilities.
$edit = array(
'tmgmt_translation_skills[und][0][language_from]' => 'en',
'tmgmt_translation_skills[und][0][language_to]' => 'de',
);
$this->drupalPost('user/' . $other_translator_same->uid . '/edit', $edit, t('Save'));
// Check that the user is not listed in the translator selection form.
$this->loginAsAdmin();
$this->drupalGet($uri['path']);
$this->assertText(t('Select translator for this job'));
$this->assertText($other_translator_same->name);
$this->assertNoText($this->local_translator->name);
$this->drupalLogin($this->local_translator);
// Configure language capabilities.
$edit = array(
'tmgmt_translation_skills[und][0][language_from]' => 'en',
'tmgmt_translation_skills[und][0][language_to]' => 'de',
);
$this->drupalPost('user/' . $this->local_translator->uid . '/edit', $edit, t('Save'));
// Check that the translator is now listed.
$this->loginAsAdmin();
$this->drupalGet($uri['path']);
$this->assertText($this->local_translator->name);
$job->requestTranslation();
// Test for job comment in the job checkout info pane.
$uri = $job->uri();
$this->drupalGet($uri['path']);
$this->assertText($job_comment);
$this->drupalLogin($this->local_translator);
// Create a second local translator with different language capabilities,
// make sure that he does not see the task.
$other_translator = $this->drupalCreateUser($this->local_translator_permissions);
$this->drupalLogin($other_translator);
// Configure language capabilities.
$edit = array(
'tmgmt_translation_skills[und][0][language_from]' => 'de',
'tmgmt_translation_skills[und][0][language_to]' => 'en',
);
$this->drupalPost('user/' . $other_translator->uid . '/edit', $edit, t('Save'));
$this->drupalGet('translate');
$this->assertNoText(t('Task for @job', array('@job' => $job->label())));
$this->drupalLogin($this->local_translator);
// Check the translate overview.
$this->drupalGet('translate');
$this->assertText(t('Task for @job', array('@job' => $job->label())));
// @todo: Fails, encoding problem?
//$this->assertText(t('@from => @to', array('@from' => 'en', '@to' => 'de')));
$edit = array(
'views_bulk_operations[0]' => $job->tjid,
);
$this->drupalPost(NULL, $edit, t('Assign to me'));
$this->assertText(t('Performed Assign to me on 1 item.'));
// Unassign again.
$edit = array(
'views_bulk_operations[0]' => $job->tjid,
);
$this->drupalPost(NULL, $edit, t('Unassign'));
$this->assertText(t('Performed Unassign on 1 item.'));
// Now test the assign link.
$this->clickLink(t('assign'));
// Log in with the translator with the same capabilities, make sure that he
// does not see the assigned task.
$this->drupalLogin($other_translator_same);
$this->drupalGet('translate');
$this->assertNoText(t('Task for @job', array('@job' => $job->label())));
$this->drupalLogin($this->local_translator);
// Translate the task.
$this->drupalGet('translate');
$this->clickLink(t('view'));
// Assert created local task and task items.
$this->assertTrue(preg_match('|translate/(\d+)|', $this->getUrl(), $matches), 'Task found');
$task = tmgmt_local_task_load($matches[1]);
$this->assertTrue($task->isPending());
$this->assertEqual($task->getCountCompleted(), 0);
$this->assertEqual($task->getCountTranslated(), 0);
$this->assertEqual($task->getCountUntranslated(), 2);
list($first_task_item, $second_task_item) = array_values($task->getItems());
$this->assertTrue($first_task_item->isPending());
$this->assertEqual($first_task_item->getCountCompleted(), 0);
$this->assertEqual($first_task_item->getCountTranslated(), 0);
$this->assertEqual($first_task_item->getCountUntranslated(), 1);
$this->assertText('test_source:test:1');
$this->assertText('test_source:test:2');
$this->assertText(t('Untranslated'));
// Translate the first item.
$this->clickLink(t('translate'));
$this->assertText(t('Dummy'));
// Job comment is present in the translate tool as well.
$this->assertText($job_comment);
$this->assertText('test_source:test:1');
// Try to complete a translation when translations are missing.
$this->drupalPost(NULL, array(), t('Save as completed'));
$this->assertText(t('Missing translation.'));
$edit = array(
'dummy|deep_nesting[translation]' => $translation1 = 'German translation of source 1',
);
$this->drupalPost(NULL, $edit, t('Save as completed'));
// Review and accept the first item.
entity_get_controller('tmgmt_job_item')->resetCache(array(1));
entity_get_controller('tmgmt_local_task')->resetCache();
entity_get_controller('tmgmt_local_task_item')->resetCache();
drupal_static_reset('tmgmt_local_task_statistics_load');
$item1 = tmgmt_job_item_load(1);
$item1->acceptTranslation();
// The first item should be accepted now, the second still in progress.
$this->drupalGet('translate/1');
$this->assertText(t('Completed'));
$this->assertText(t('Untranslated'));
$task = tmgmt_local_task_load($task->tltid);
$this->assertTrue($task->isPending());
$this->assertEqual($task->getCountCompleted(), 1);
$this->assertEqual($task->getCountTranslated(), 0);
$this->assertEqual($task->getCountUntranslated(), 1);
list($first_task_item, $second_task_item) = array_values($task->getItems());
$this->assertTrue($first_task_item->isClosed());
$this->assertEqual($first_task_item->getCountCompleted(), 1);
$this->assertEqual($first_task_item->getCountTranslated(), 0);
$this->assertEqual($first_task_item->getCountUntranslated(), 0);
$this->assertTrue($second_task_item->isPending());
$this->assertEqual($second_task_item->getCountCompleted(), 0);
$this->assertEqual($second_task_item->getCountTranslated(), 0);
$this->assertEqual($second_task_item->getCountUntranslated(), 1);
// Translate the second item but do not mark as translated it yet.
$this->clickLink(t('translate'));
$edit = array(
'dummy|deep_nesting[translation]' => $translation2 = 'German translation of source 2',
);
$this->drupalPost(NULL, $edit, t('Save'));
// The first item is still completed, the second still untranslated.
$this->assertText(t('Completed'));
$this->assertText(t('Untranslated'));
entity_get_controller('tmgmt_local_task')->resetCache();
entity_get_controller('tmgmt_local_task_item')->resetCache();
drupal_static_reset('tmgmt_local_task_statistics_load');
$task = tmgmt_local_task_load($task->tltid);
$this->assertTrue($task->isPending());
$this->assertEqual($task->getCountCompleted(), 1);
$this->assertEqual($task->getCountTranslated(), 0);
$this->assertEqual($task->getCountUntranslated(), 1);
list($first_task_item, $second_task_item) = array_values($task->getItems());
$this->assertTrue($first_task_item->isClosed());
$this->assertEqual($first_task_item->getCountCompleted(), 1);
$this->assertEqual($first_task_item->getCountTranslated(), 0);
$this->assertEqual($first_task_item->getCountUntranslated(), 0);
$this->assertTrue($second_task_item->isPending());
$this->assertEqual($second_task_item->getCountCompleted(), 0);
$this->assertEqual($second_task_item->getCountTranslated(), 0);
$this->assertEqual($second_task_item->getCountUntranslated(), 1);
// Mark the data item as translated but don't save the task item as
// completed.
$this->clickLink(t('translate'));
$this->drupalPost(NULL, array(), t('✓'));
entity_get_controller('tmgmt_local_task')->resetCache();
entity_get_controller('tmgmt_local_task_item')->resetCache();
drupal_static_reset('tmgmt_local_task_statistics_load');
$task = tmgmt_local_task_load($task->tltid);
$this->assertTrue($task->isPending());
$this->assertEqual($task->getCountCompleted(), 1);
$this->assertEqual($task->getCountTranslated(), 1);
$this->assertEqual($task->getCountUntranslated(), 0);
list($first_task_item, $second_task_item) = array_values($task->getItems());
$this->assertTrue($first_task_item->isClosed());
$this->assertEqual($first_task_item->getCountCompleted(), 1);
$this->assertEqual($first_task_item->getCountTranslated(), 0);
$this->assertEqual($first_task_item->getCountUntranslated(), 0);
$this->assertTrue($second_task_item->isPending());
$this->assertEqual($second_task_item->getCountCompleted(), 0);
$this->assertEqual($second_task_item->getCountTranslated(), 1);
$this->assertEqual($second_task_item->getCountUntranslated(), 0);
// Check the job data.
entity_get_controller('tmgmt_job')->resetCache(array($job->tjid));
entity_get_controller('tmgmt_job_item')->resetCache();
$job = tmgmt_job_load($job->tjid);
list($item1, $item2) = array_values($job->getItems());
// The text in the first item should be available for review, the
// translation of the second item not.
$this->assertEqual($item1->getData(array('dummy', 'deep_nesting', '#translation', '#text')), $translation1);
$this->assertEqual($item2->getData(array('dummy', 'deep_nesting', '#translation', '#text')), '');
// Check the overview page, the task should still show in progress.
$this->drupalGet('translate');
$this->assertText(t('Pending'));
// Mark the second item as completed now.
$this->clickLink(t('view'));
$this->clickLink(t('translate'));
$this->drupalPost(NULL, array(), t('Save as completed'));
// Review and accept the second item.
entity_get_controller('tmgmt_job_item')->resetCache(array(2));
entity_get_controller('tmgmt_local_task')->resetCache();
entity_get_controller('tmgmt_local_task_item')->resetCache();
drupal_static_reset('tmgmt_local_task_statistics_load');
$item1 = tmgmt_job_item_load(2);
$item1->acceptTranslation();
// Refresh the page.
$this->drupalGet($this->url);
$task = tmgmt_local_task_load($task->tltid);
$this->assertTrue($task->isClosed());
$this->assertEqual($task->getCountCompleted(), 2);
$this->assertEqual($task->getCountTranslated(), 0);
$this->assertEqual($task->getCountUntranslated(), 0);
list($first_task_item, $second_task_item) = array_values($task->getItems());
$this->assertTrue($first_task_item->isClosed());
$this->assertEqual($first_task_item->getCountCompleted(), 1);
$this->assertEqual($first_task_item->getCountTranslated(), 0);
$this->assertEqual($first_task_item->getCountUntranslated(), 0);
$this->assertTrue($second_task_item->isClosed());
$this->assertEqual($second_task_item->getCountCompleted(), 1);
$this->assertEqual($second_task_item->getCountTranslated(), 0);
$this->assertEqual($second_task_item->getCountUntranslated(), 0);
// We should have been redirect back to the overview, the task should be
// completed now.
$this->assertNoText($task->getJob()->label());
$this->clickLink(t('Closed'));
$this->assertText($task->getJob()->label());
$this->assertText(t('Completed'));
entity_get_controller('tmgmt_job')->resetCache(array($job->tjid));
entity_get_controller('tmgmt_job_item')->resetCache();
$job = tmgmt_job_load($job->tjid);
list($item1, $item2) = array_values($job->getItems());
// Job was accepted and finished automatically due to the default approve
// setting.
$this->assertTrue($job->isFinished());
$this->assertEqual($item1->getData(array('dummy', 'deep_nesting', '#translation', '#text')), $translation1);
$this->assertEqual($item2->getData(array('dummy', 'deep_nesting', '#translation', '#text')), $translation2);
// Delete the job, make sure that the corresponding task and task items were
// deleted.
$job->delete();
$this->assertFalse(tmgmt_local_task_item_load($task->tltid));
$this->assertFalse($task->getItems());
}
/**
* Test the allow all setting.
*/
function testAllowAll() {
$translator = tmgmt_translator_load('local');
// Create a job and request a local translation.
$this->loginAsTranslator();
$job = $this->createJob();
$job->translator = $translator->name;
$job->addItem('test_source', 'test', '1');
$job->addItem('test_source', 'test', '2');
$this->assertFalse($job->requestTranslation(), 'Translation request was denied.');
// Now enable the setting.
$translator->settings['allow_all'] = TRUE;
$translator->save();
$this->assertIdentical(NULL, $job->requestTranslation(), 'Translation request was successfull');
$this->assertTrue($job->isActive());
}
function testCapabilitiesAPI() {
$this->setEnvironment('fr');
$this->setEnvironment('ru');
$this->setEnvironment('it');
$all_translators = array();
$translator1 = $this->drupalCreateUser($this->local_translator_permissions);
$all_translators[$translator1->uid] = $translator1->name;
$this->drupalLogin($translator1);
$edit = array(
'tmgmt_translation_skills[und][0][language_from]' => 'en',
'tmgmt_translation_skills[und][0][language_to]' => 'de',
);
$this->drupalPost('user/' . $translator1->uid . '/edit', $edit, t('Save'));
$translator2 = $this->drupalCreateUser($this->local_translator_permissions);
$all_translators[$translator2->uid] = $translator2->name;
$this->drupalLogin($translator2);
$edit = array(
'tmgmt_translation_skills[und][0][language_from]' => 'en',
'tmgmt_translation_skills[und][0][language_to]' => 'ru',
);
$this->drupalPost('user/' . $translator2->uid . '/edit', $edit, t('Save'));
$edit = array(
'tmgmt_translation_skills[und][1][language_from]' => 'en',
'tmgmt_translation_skills[und][1][language_to]' => 'fr',
);
$this->drupalPost('user/' . $translator2->uid . '/edit', $edit, t('Save'));
$edit = array(
'tmgmt_translation_skills[und][2][language_from]' => 'fr',
'tmgmt_translation_skills[und][2][language_to]' => 'it',
);
$this->drupalPost('user/' . $translator2->uid . '/edit', $edit, t('Save'));
$translator3 = $this->drupalCreateUser($this->local_translator_permissions);
$all_translators[$translator3->uid] = $translator3->name;
$this->drupalLogin($translator3);
$edit = array(
'tmgmt_translation_skills[und][0][language_from]' => 'fr',
'tmgmt_translation_skills[und][0][language_to]' => 'ru',
);
$this->drupalPost('user/' . $translator3->uid . '/edit', $edit, t('Save'));
$edit = array(
'tmgmt_translation_skills[und][1][language_from]' => 'it',
'tmgmt_translation_skills[und][1][language_to]' => 'en',
);
$this->drupalPost('user/' . $translator3->uid . '/edit', $edit, t('Save'));
// Test target languages.
$target_languages = tmgmt_local_supported_target_languages('fr');
$this->assertTrue(isset($target_languages['it']));
$this->assertTrue(isset($target_languages['ru']));
$target_languages = tmgmt_local_supported_target_languages('en');
$this->assertTrue(isset($target_languages['fr']));
$this->assertTrue(isset($target_languages['ru']));
// Test language pairs.
$this->assertEqual(tmgmt_local_supported_language_pairs(), array (
'en__de' =>
array (
'source_language' => 'en',
'target_language' => 'de',
),
'en__ru' =>
array (
'source_language' => 'en',
'target_language' => 'ru',
),
'en__fr' =>
array (
'source_language' => 'en',
'target_language' => 'fr',
),
'fr__it' =>
array (
'source_language' => 'fr',
'target_language' => 'it',
),
'fr__ru' =>
array (
'source_language' => 'fr',
'target_language' => 'ru',
),
'it__en' =>
array (
'source_language' => 'it',
'target_language' => 'en',
),
));
$this->assertEqual(tmgmt_local_supported_language_pairs('fr', array($translator2->uid)), array (
'fr__it' =>
array (
'source_language' => 'fr',
'target_language' => 'it',
),
));
// Test if we got all translators.
$translators = tmgmt_local_translators();
foreach ($all_translators as $uid => $name) {
if (!isset($translators[$uid])) {
$this->fail('Expected translator not present');
}
if (!in_array($name, $all_translators)) {
$this->fail('Expected translator name not present');
}
}
// Only translator2 has such capabilities.
$translators = tmgmt_local_translators('en', array('ru', 'fr'));
$this->assertTrue(isset($translators[$translator2->uid]));
}
/**
* Test permissions for the tmgmt_local VBO actions.
*/
function testVBOPermissions() {
$translator = tmgmt_translator_load('local');
$job = $this->createJob();
$job->translator = $translator->name;
$job->settings['job_comment'] = $job_comment = 'Dummy job comment';
$job->addItem('test_source', 'test', '1');
$job->addItem('test_source', 'test', '2');
// Create another local translator with the required capabilities.
$local_translator = $this->loginAsTranslator($this->local_translator_permissions);
// Configure language capabilities.
$edit = array(
'tmgmt_translation_skills[und][0][language_from]' => 'en',
'tmgmt_translation_skills[und][0][language_to]' => 'de',
);
$this->drupalPost('user/' . $local_translator->uid . '/edit', $edit, t('Save'));
$job->requestTranslation();
$this->drupalGet('translate');
$this->assertFieldById('edit-rules-componentrules-tmgmt-local-task-assign-to-me', t('Assign to me'));
$this->assertFieldById('edit-rules-componentrules-tmgmt-local-task-unassign', t('Unassign'));
// Login as admin and check VBO submit actions are present.
$this->loginAsAdmin(array('administer translation tasks'));
$this->drupalGet('manage-translate');
$this->assertFieldById('edit-actionviews-bulk-operations-argument-selector-action--2', t('Assign to...'));
$this->assertFieldById('edit-actionviews-bulk-operations-argument-selector-action', t('Assign to...'));
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* Field handler which shows the link for translating translation task items.
*
* @ingroup views_field_handlers
*/
class tmgmt_local_task_handler_field_item_operations extends views_handler_field_entity {
function render($values) {
global $user;
/**
* @var TMGMTLocalTaskItem $item
*/
$item = $this->get_value($values);
$element = array();
$element['#theme'] = 'links';
$element['#attributes'] = array('class' => array('links', 'inline'));
// Only allow to translate if the job is assigned to this user.
if (entity_access('view', 'tmgmt_local_task_item', $item) && $item->getTask()->tuid == $user->uid) {
$element['#links']['translate'] = array(
'href' => 'translate/' . $item->tltid . '/item/' . $item->tltiid,
'attributes' => array(
'title' => $item->isPending() ? t('Translate') : t('View'),
),
'title' => $item->isPending() ? t('translate') : t('view'),
);
}
return drupal_render($element);
}
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* Field handler to show the amount of job items per task.
*
* @ingroup views_field_handlers
*/
class tmgmt_local_task_handler_field_job_item_count extends views_handler_field {
/**
* @var views_plugin_query_default
*/
var $query;
function option_definition() {
$options = parent::option_definition();
$options['state'] = array('default' => '');
return $options;
}
function options_form(&$form, &$form_state) {
parent::options_form($form, $form_state);
$options = array('' => t('- All -'));
$options += tmgmt_job_item_states();
$form['state'] = array(
'#title' => t('Job item state'),
'#description' => t('Count only job items of a certain state.'),
'#type' => 'select',
'#options' => $options,
'#default_value' => $this->options['state'],
);
}
function use_group_by() {
return FALSE;
}
function query() {
$this->ensure_my_table();
// Therefore construct the join.
$join = new views_join();
$join->definition['left_table'] = $this->table_alias;
$join->definition['left_field'] = 'tjid';
$join->definition['table'] = 'tmgmt_job_item';
$join->definition['field'] = 'tjid';
$join->definition['type'] = 'LEFT';
if (!empty($this->options['state'])) {
$join->extra = array(array(
'field' => 'state',
'value' => $this->options['state']
));
}
$join->construct();
// Add the join to the tmgmt_job_item table.
$this->table_alias = $this->query->add_table('tmgmt_job_item', $this->relationship, $join);
// And finally add the count of the job items field.
$params = array('function' => 'count');
$this->field_alias = $this->query->add_field($this->table_alias, 'tjiid', NULL, $params);
$this->add_additional_fields();
}
}

View File

@@ -0,0 +1,17 @@
<?php
/**
* Field handler which shows the loop count.
*
* @ingroup views_field_handlers
*/
class tmgmt_local_task_handler_field_loop_count extends views_handler_field_entity {
/**
* {@inheritdoc}
*/
function render($values) {
$object = $this->get_value($values);
return $object->getLoopCount();
}
}

View File

@@ -0,0 +1,79 @@
<?php
/**
* Field handler which shows the operations for a task.
*
* @todo Remove this once http://drupal.org/node/1435662 is through.
*
* @ingroup views_field_handlers
*/
class tmgmt_local_task_handler_field_operations extends views_handler_field_entity {
function render($values) {
/**
* @var TMGMTLocalTask $task
*/
$task = $this->get_value($values);
$element = array();
$element['#theme'] = 'links';
$element['#attributes'] = array('class' => array('links', 'inline'));
$uri = $task->uri();
if (entity_access('view', 'tmgmt_local_task', $task)) {
$element['#links']['view'] = array(
'href' => $uri['path'],
'query' => array('destination' => current_path()),
'title' => t('view'),
);
}
if (user_access('administer translation tasks') && tmgmt_local_translation_access($task) && empty($task->tuid)) {
$element['#links']['assign'] = array(
'href' => 'manage-translate/assign-tasks/' . $task->tltid,
'query' => array('destination' => current_path()),
'attributes' => array(
'title' => t('Assign'),
),
'title' => t('assign'),
);
}
elseif (tmgmt_local_translation_access($task) && empty($task->tuid)) {
$element['#links']['assign_to_me'] = array(
'href' => 'translate/' . $task->tltid . '/assign-to-me',
'query' => array('destination' => current_path()),
'attributes' => array(
'title' => t('Assign to me'),
),
'title' => t('assign'),
);
}
elseif (tmgmt_local_translation_access($task) && empty($task->tuid)) {
$element['#links']['assign_to_me'] = array(
'href' => 'translate/' . $task->tltid . '/assign-to-me',
'query' => array('destination' => current_path()),
'attributes' => array(
'title' => t('Assign to me'),
),
'title' => t('assign'),
);
}
if (!empty($task->tuid) && entity_access('unassign', 'tmgmt_local_task', $task)) {
$element['#links']['unassign'] = array(
'href' => 'translate/' . $task->tltid . '/unassign',
'query' => array('destination' => current_path()),
'attributes' => array(
'title' => t('Unassign'),
),
'title' => t('unassign'),
);
}
if (entity_access('delete', 'tmgmt_local_task', $task)) {
$element['#links']['delete'] = array(
'href' => $uri['path'] . '/delete',
'query' => array('destination' => current_path()),
'title' => t('delete'),
);
}
return drupal_render($element);
}
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* Field handler which shows the progressbar.
*
* @ingroup views_field_handlers
*/
class tmgmt_local_task_handler_field_progress extends tmgmt_handler_field_tmgmt_progress {
/**
* Prefetch statistics for all jobs.
*/
function pre_render(&$values) {
parent::pre_render($values);
// In case of tasks, pre-fetch the statistics in a single query and add them
// to the static cache.
if ($this->entity_type == 'tmgmt_task') {
$tltids = array();
foreach ($values as $value) {
$tltids[] = $value->tjid;
}
tmgmt_local_task_statistics_load($tltids);
}
}
/**
* {@inheritdoc}
*/
function render($values) {
$object = $this->get_value($values);
$counts = array(
'@untranslated' => $object->getCountUntranslated(),
'@translated' => $object->getCountTranslated(),
'@completed' => $object->getCountCompleted(),
);
$id = $object->internalIdentifier();
if (module_exists('google_chart_tools')) {
draw_chart($this->build_progressbar_settings($id, $counts));
return '<div id="progress' . $id . '"></div>';
}
$title = t('Untranslated: @untranslated, translated: @translated, completed: @completed.', $counts);
return sprintf('<span title="%s">%s</span>', $title, implode('/', $counts));
}
/**
* {@inheritdoc}
*/
function build_progressbar_settings($id, $counts, $prefix = 'progress') {
$settings['chart'][$prefix . $id] = array(
'header' => array(t('Untranslated'), t('Translated'), t('Completed')),
'rows' => array(
array($counts['@untranslated'], $counts['@translated'], $counts['@completed']),
),
'columns' => array(''),
'chartType' => 'PieChart',
'containerId' => $prefix . $id,
'options' => array(
'backgroundColor' => 'transparent',
'colors' => array('#60ff60', '#ffff00', '#6060ff'),
'forceIFrame' => FALSE,
'chartArea' => array(
'left' => 0,
'top' => 0,
'width' => '50%',
'height' => '100%',
),
'fontSize' => 9,
'title' => t('Progress'),
'titlePosition' => 'none',
'width' => 60,
'height' => 50,
'isStacked' => TRUE,
'legend' => array('position' => 'none'),
'pieSliceText' => 'none',
)
);
return $settings;
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* Field handler which shows the word count.
*
* @ingroup views_field_handlers
*/
class tmgmt_local_task_handler_field_wordcount extends views_handler_field_entity {
/**
* Prefetch statistics for all jobs.
*/
function pre_render(&$values) {
parent::pre_render($values);
// In case of jobs, pre-fetch the statistics in a single query and add them
// to the static cache.
if ($this->entity_type == 'tmgmt_task') {
$tltids = array();
foreach ($values as $value) {
$tltids[] = $value->tjid;
}
tmgmt_local_task_statistics_load($tltids);
}
}
/**
* {@inheritdoc}
*/
function render($values) {
$object = $this->get_value($values);
return $object->getWordCount();
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* Field handler which shows the link for assign translation task to selected
* user.
*
* @ingroup views_field_handlers
*/
class tmgmt_local_task_handler_filter_eligible extends views_handler_filter {
/**
* {@inheritdoc}
*/
public function query() {
$this->ensure_my_table();
$source = $this->table_alias . '.source_language';
$target = $this->table_alias . '.target_language';
// Add a new group for the language capabilities, which are a set of source
// and target language combinations.
$this->query->set_where_group('OR', 'eligible');
// Return all language capabilities for the current user.
foreach (tmgmt_local_supported_language_pairs(NULL, array($GLOBALS['user']->uid)) as $key => $capability) {
$key = str_replace('-', '_', $key);
$arguments = array(':source_' . $key => $capability['source_language'], ':target_' . $key => $capability['target_language']);
$this->query->add_where_expression('eligible', "$source = :source_$key AND $target = :target_$key", $arguments);
}
}
}

View File

@@ -0,0 +1,95 @@
<?php
/**
* @file
* Contains Views controllers for the translation management local task module.
*/
/**
* Views controller class for the local task entity.
*/
class TMGMTLocalTaskViewsController extends EntityDefaultViewsController {
/**
* {@inheritdoc}
*/
public function views_data() {
$data = parent::views_data();
$data['tmgmt_local_task']['operations'] = array(
'title' => t('Operations'),
'help' => t('Displays a list of operations which are available for a task.'),
'real field' => 'tltid',
'field' => array(
'handler' => 'tmgmt_local_task_handler_field_operations',
),
);
$data['tmgmt_local_task']['progress'] = array(
'title' => t('Progress'),
'help' => t('Displays the progress of a job.'),
'real field' => 'tltid',
'field' => array(
'handler' => 'tmgmt_local_task_handler_field_progress',
),
);
$data['tmgmt_local_task']['word_count'] = array(
'title' => t('Word count'),
'help' => t('Displays the word count of a job.'),
'real field' => 'tltid',
'field' => array(
'handler' => 'tmgmt_local_task_handler_field_wordcount',
),
);
$data['tmgmt_local_task']['item_count'] = array(
'title' => t('Job item count'),
'help' => t('Show the amount of job items per task (per job item status)'),
'real field' => 'tltid',
'field' => array(
'handler' => 'tmgmt_local_task_handler_field_job_item_count',
),
);
$data['tmgmt_job']['eligible'] = array(
'title' => t('Eligible'),
'help' => t('Limit translation tasks to those that the user can translate'),
'real field' => 'tltid',
'filter' => array(
'handler' => 'tmgmt_local_task_handler_filter_eligible',
),
);
// Manager handlers.
$data['tmgmt_job']['task'] = array(
'title' => t('Translation task'),
'help' => t('Get the translation task of the job'),
'relationship' => array(
'base' => 'tmgmt_local_task',
'base field' => 'tjid',
'real field' => 'tjid',
'label' => t('Job'),
),
);
return $data;
}
}
/**
* Views controller class for the translation task item entity.
*/
class TMGMTLocalTaskItemViewsController extends EntityDefaultViewsController {
/**
* {@inheritdoc}
*/
public function views_data() {
$data = parent::views_data();
$data['tmgmt_local_task_item']['operations'] = array(
'title' => t('Operations'),
'help' => t('Displays a list of operations which are available for a task item.'),
'real field' => 'tltiid',
'field' => array(
'handler' => 'tmgmt_local_task_handler_field_item_operations',
),
);
return $data;
}
}

View File

@@ -0,0 +1,81 @@
<?php
$view = new view();
$view->name = 'tmgmt_local_task_items';
$view->description = '';
$view->tag = 'default';
$view->base_table = 'tmgmt_local_task_item';
$view->human_name = 'Translation Task Items';
$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'] = 'Translation Task Items';
$handler->display->display_options['use_more_always'] = FALSE;
$handler->display->display_options['access']['type'] = 'none';
$handler->display->display_options['cache']['type'] = 'none';
$handler->display->display_options['query']['type'] = 'views_query';
$handler->display->display_options['exposed_form']['type'] = 'basic';
$handler->display->display_options['pager']['type'] = 'full';
$handler->display->display_options['pager']['options']['items_per_page'] = '20';
$handler->display->display_options['style_plugin'] = 'table';
/* Relationship: Translation Task Item: Tltid */
$handler->display->display_options['relationships']['tltid']['id'] = 'tltid';
$handler->display->display_options['relationships']['tltid']['table'] = 'tmgmt_local_task_item';
$handler->display->display_options['relationships']['tltid']['field'] = 'tltid';
/* Relationship: Translation Task Item: Tjiid */
$handler->display->display_options['relationships']['tjiid']['id'] = 'tjiid';
$handler->display->display_options['relationships']['tjiid']['table'] = 'tmgmt_local_task_item';
$handler->display->display_options['relationships']['tjiid']['field'] = 'tjiid';
/* Field: Translation Management Job Item: Label */
$handler->display->display_options['fields']['label']['id'] = 'label';
$handler->display->display_options['fields']['label']['table'] = 'tmgmt_job_item';
$handler->display->display_options['fields']['label']['field'] = 'label';
$handler->display->display_options['fields']['label']['relationship'] = 'tjiid';
/* Field: Translation Task Item: Status */
$handler->display->display_options['fields']['status']['id'] = 'status';
$handler->display->display_options['fields']['status']['table'] = 'tmgmt_local_task_item';
$handler->display->display_options['fields']['status']['field'] = 'status';
/* Field: Translation Task Item: Operations */
$handler->display->display_options['fields']['operations']['id'] = 'operations';
$handler->display->display_options['fields']['operations']['table'] = 'tmgmt_local_task_item';
$handler->display->display_options['fields']['operations']['field'] = 'operations';
/* Contextual filter: Translation Task Item: Tltid */
$handler->display->display_options['arguments']['tltid']['id'] = 'tltid';
$handler->display->display_options['arguments']['tltid']['table'] = 'tmgmt_local_task_item';
$handler->display->display_options['arguments']['tltid']['field'] = 'tltid';
$handler->display->display_options['arguments']['tltid']['default_action'] = 'empty';
$handler->display->display_options['arguments']['tltid']['default_argument_type'] = 'fixed';
$handler->display->display_options['arguments']['tltid']['summary']['number_of_records'] = '0';
$handler->display->display_options['arguments']['tltid']['summary']['format'] = 'default_summary';
$handler->display->display_options['arguments']['tltid']['summary_options']['items_per_page'] = '25';
/* Display: Block */
$handler = $view->new_display('block', 'Block', 'block');
$handler->display->display_options['defaults']['hide_admin_links'] = FALSE;
$translatables['tmgmt_local_task_items'] = array(
t('Master'),
t('Translation Task Items'),
t('more'),
t('Apply'),
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('Translation Task'),
t('Translation Management Job Item'),
t('Label'),
t('Status'),
t('Operations'),
t('All'),
t('Block'),
);