* @license PHP License * @package WB * @subpackage content */ /** * Load base class */ WBClass::load('WBContent'); /** * Content component: Translation Manager * * Frontend to translate messages stored in patI18n importer module Wombat * * @version 1.0.0 * @package WB * @subpackage content */ class WBContent_Locale_Translator extends WBContent { /** * Parameter list * * action: selected action, one of: overview, list, edit, rm, translate, extract and orphancheck * requiredgroup: user group for uses that are alloed to translate messages * requiredlangperm: user group with permission to edit original messages and extract new messages * requiredgroup: user group for uses that are alloed to translate messages * domain: text domain * lang: translation language * clang: language that represents "C", the source language * goto: page to display * inputclassname: custom class name for input fields * searchmsgid: search string * orphan: select orphans, too * * * @var array */ protected $config = array( 'action' => 'overview', 'requiredgroup' => 'nls-translator', 'requiredlangperm' => 0, 'requiredgroupedit' => 'contenteditor', 'searchmsgid' => '', 'domain' => '', 'translationstatus' => 'untranslated', 'lang' => 'de_DE', 'clang' => 'en_GB', 'goto' => 0, 'inputclassname' => '', 'orphan' => '0', 'msglength' => -1 ); /** * table access * @var WBDatasource_Table */ private $table; /** * I18n message importer * @var patI18n_Importer */ private $imp; /** * locale settings * @param WBConfig */ private $locale; /** * temporary varibable holding current dataset * @var array */ private $current = array(); /** * run * * run component * * @return array parameter list */ public function run() { if (!$this->isUserInGroup($this->config['requiredgroup'])) { $this->loadTemplates('anon'); return $this->config; } if (!empty($this->config['searchmsgid'])) { $this->config['searchmsgid'] = str_replace('+', ' ', $this->config['searchmsgid']); } $this->locale = WBClass::create('WBConfig'); $this->locale->load('locale'); // set current language $available = $this->locale->get('locale/languages/available', array()); if (!in_array($this->config['lang'], $available)) { $this->config['lang'] = $available[0]; } $this->config['lang_short'] = substr($this->config['lang'], 0, 2); $this->config['searchmsgid_url'] = urlencode($this->config['searchmsgid']); $this->tmpl->addGlobalVars($this->config, 'CONFIG_'); // don't automatically translate anything $this->table = WBClass::create('WBDatasource_Table'); $this->table->switchTranslation(false); // check permissions if (in_array($this->config['action'], array('list', 'edit', 'rm', 'extract','orphancheck')) && !$this->isUserInGroup($this->config['requiredgroupedit'])) { $this->config['action'] = 'list'; } $this->tmpl->addGlobalVar('lang_translatable', 1); if ($this->config['requiredlangperm']) { $langs = $this->table->get('nlslangtranslator', null, $this->user->getId()); $found = false; $av = array(); foreach ($langs as $l) { if ($l['lang'] == $this->config['lang']) { $found = true; } if (in_array($l['lang'], $available)) { $av[] = $l['lang']; } } if (!$found) { $this->config['action'] = 'list'; $this->tmpl->addGlobalVar('lang_translatable', 0); } if (!in_array($this->config['action'], array('list', 'translate')) && count($available) != count($av)) { $this->config['action'] = 'list'; $this->tmpl->addGlobalVar('lang_translatable', 0); } } switch ($this->config['action']) { case 'list': $this->listMsg(); break; case 'edit': $this->editMsg(); break; case 'rm': $this->rmMsg(); break; case 'translate': $this->translateMsg(); break; case 'extract': $this->runExtractor(); break; case 'orphancheck': $this->runOrphanCheck(); break; case 'overview': default: $this->loadTemplates('overview'); $this->listExtractors($this->getExtrators()); break; } return $this->config; } /** * list messages * * Display list of translatable messages */ private function listMsg() { $this->processForm('list', $this->config, 'list'); $this->loadTemplates('listMsg'); $clause = array(); $lang = $this->config['lang']; // message length $msglength = intval($this->config['msglength']); if (0 < $msglength) { $clause[] = array( 'type' => 'native', 'clause' => sprintf('char_length(msg) > %s', $msglength) ); } else if (-1 > $msglength) { $msglength = abs($msglength); $clause[] = array( 'type' => 'native', 'clause' => sprintf('char_length(msg) < %s', $msglength) ); } // orphan flag if (1 == intval($this->config['orphan'])) { $clause[] = array( 'field' => 'orphan', 'value' => 1 ); } if ($this->config['clang'] != $lang) { switch ($this->config['translationstatus']) { case 'untranslated': $clause[] = array( 'field' => 'trans_' . $lang, 'relation' => 'null', 'value' => 1 ); break; case 'translated': $clause[] = array( 'field' => 'trans_' . $lang, 'relation' => 'null', 'value' => 0 ); break; case 'fuzzy': $clause[] = array( 'field' => 'trans_' . $lang, 'relation' => 'null', 'value' => 0 ); $clause[] = array( 'field' => 'changed_' . $lang, 'relation' => 'lt', 'foreign' => 'nlsmsg', 'value' => 'changed', 'valuetype' => 'foreign' ); break; default: break; } } // textdomain if (!empty($this->config['domain'])) { $clause[] = array( 'field' => 'domain', 'value' => $this->config['domain'] ); } // search for message if (!empty($this->config['searchmsgid'])) { $this->addSearchClause($lang, $this->config['searchmsgid'], $clause); } $options = array( 'limit' => 50, 'order' => array( array( 'field' => 'created', 'asc' => 0 ) ), 'column' => array('singular', 'msg', 'domain', 'created', 'orphan') ); // select translations if ('en' != $lang) { $options['column'][] = array( 'field' => 'trans_' . $lang, 'as' => 'translation' ); $options['column'][] = array( 'field' => 'changed_' . $lang, 'as' => 'translation_changed' ); $options['column'][] = array( 'field' => 'min_' . $lang, 'as' => 'translation_min' ); } $pager = $this->table->getPager($this->part . ':' . __CLASS__ . ':list', 'nlsmsg', null, $clause, $options); /** @var $pager WBDatasource_Pager */ $this->tmpl->addGlobalVars($pager->browse($this->req->get('goto', 0)), 'PAGER_'); $list = $pager->get(); foreach ($list as &$l) { $l['msg'] = $this->quote($l['msg']); $l['translation'] = $this->quote($l['translation']); } $this->tmpl->addRows('msg_list_entry', $list); } /** * Inject search string clause * * @param string $lang * @param string $search * @param array $clause */ private function addSearchClause($lang, $search, &$clause) { if (preg_match_all('/(\d+)/', $search, $match)) { $tmp = preg_replace('/(\d+)/', '', $search); $tmp = trim($tmp); if (empty($tmp)) { $clause[] = array( 'field' => $this->table->getIdentifier('nlsmsg'), 'relation' => 'IN', 'value' => $match[1] ); return; } } $search = trim($search); if (empty($search)) { return; } $mclause = array(); $mclause[] = array( 'field' => 'msg', 'relation' => 'like', 'value' => $search ); $mclause[] = array( 'field' => 'msg_org', 'relation' => 'like', 'value' => $search ); $mclause[] = array( 'field' => 'trans_' . $lang, 'relation' => 'like', 'value' => $search ); $clause[] = array( 'type' => 'complex', 'clause' => $mclause, 'bond' => 'or' ); } /** * form processing for list options * * Update config with search options * * @param patForms $form * @param array $values */ protected function onListValid($form, $values) { $keys = array( 'searchmsgid', 'translationstatus', 'domain', 'orphan' ); foreach ($keys as $key) { if (isset($values[$key])) { $this->config[$key] = $values[$key]; } } $this->loadTemplates('list'); $this->renderForm($form); return false; } /** * edit original message */ private function editMsg() { if (!$this->load()) { return $this->listMsg(); } $this->current['msg'] = $this->quote($this->current['msg']); $this->processForm('edit', $this->current); } /** * form processing for after message was edited * * Update message in database * * @param patForms $form * @param array $values */ protected function onEditValid($form, $values) { $save = array( 'msg' => $this->quote($values['msg'], true), 'changed' => gmdate('Y-m-d H:i:s') ); $this->save($save); $this->listMsg(); return false; } /** * remove message and translations */ private function rmMsg() { if (!$this->load()) { return $this->listMsg(); } $this->table->delete('nlsmsg', $this->current['id']); return $this->listMsg(); } /** * translate message */ private function translateMsg() { if (!$this->load()) { return $this->listMsg(); } $trans = 'trans_' . $this->config['lang']; $this->current['msg'] = $this->quote($this->current['msg']); $this->current['msg_html'] = $this->quote($this->current['msg_html']); if (empty($this->current[$trans])) { $this->current[$trans] = $this->current['msg']; } $this->current['translation'] = $this->quote($this->current[$trans]); $this->tmpl->addGlobalVars($this->current); $this->processForm('translate', $this->current); } /** * form processing for after message was edited * * Update message in database * * @param patForms $form * @param array $values */ protected function onTranslateValid($form, $values) { $lang = $this->config['lang']; if (empty($values['translation'])) { $values['translation'] = null; } else { $values['translation'] = $this->quote($values['translation'], true); } $save = array( 'trans_' . $lang => $values['translation'], 'changed_' . $lang => gmdate('Y-m-d H:i:s'), 'min_' . $lang => 0 ); $this->save($save); $this->listMsg(); return false; } /** * Check for Orphan messages * * */ public function runOrphanCheck() { $this->loadTemplates('orphan'); // delete orphans if ('yes' == $this->req->get('delete', 'no')) { $clause = array(); $clause[] = array( 'field' => 'orphan', 'value' => 1 ); $this->table->delete('nlsmsg', null, null, $clause); $this->addOrphanCount(); } // check for orphans if ('yes' != $this->req->get('check', 'no')) { $this->addOrphanCount(); return; } $list = $this->getExtrators(); if (0 >= count($list)) { return; } // mark all messages as orphan to start $save = array( 'orphan' => 1 ); $clause = array(); $clause[] = array( 'field' => $this->table->getIdentifier('nlsmsg'), 'value' => 0, 'relation' => 'NOT' ); $this->table->save('nlsmsg', null, $save, $clause); $this->startImporter(); $this->imp->setMode(patI18n_Importer_Wombat::MODE_ORPHANCHECK); foreach ($list as &$l) { $ex = WBClass::create('WBNLS_Extractor_' . $l['name']); $ex->setImporter($this->imp); //$ex->setMode(patI18n_Importer_Wombat::MODE_ORPHANCHECK); $l['cnt'] = $ex->run(); } $this->addOrphanCount(); $this->listExtractors($list); } private function addOrphanCount() { $globs = array( 'normal' => 0, 'orphan' => 0, 'total' => 0 ); $options = array( 'groupby' => array( array( 'field' => 'orphan' ) ), 'column' => array( 'orphan', array( 'field' => 'orphan', 'function' => 'count', 'as' => 'cnt' ) ) ); $cnt = $this->table->get('nlsmsg', null, null, array(), $options); foreach ($cnt as $c) { if ($c['orphan']) { $globs['orphan'] = $c['cnt']; } else { $globs['normal'] = $c['cnt']; } } $globs['total'] = $globs['normal'] + $globs['orphan']; $this->tmpl->addGlobalVars($globs, 'count_'); } /** * load message data set * * Use mid from request and load row from database. The actual data * will be available in $this->current * * @return bool true on success */ private function load() { $id = $this->req->get('mid', null); if (isset($this->current['id']) && $this->current['id'] == $id) { return true; } if (empty($id)) { $this->current = array(); return false; } $data = $this->table->get('nlsmsg', $id); if (empty($data) || 1 != count($data)) { $this->current = array(); return false; } $this->current = $data[0]; $this->current['id'] = $id; $this->current['msg_html'] = htmlspecialchars($this->current['msg']); return true; } /** * save record in database * * Load current record and update values * * @see load() * @param array $save * @return bool true on success */ private function save($save) { if (!$this->load()) { return false; } $this->table->save('nlsmsg', $this->current['id'], $save); return true; } /** * extract translatable strings * * Use extractor to load find strings */ private function runExtractor() { $this->loadTemplates('extractor'); $list = $this->getExtrators(); $this->listExtractors($list); $glob = array( 'name' => $this->req->get('extractor'), 'title' => '', 'total' => 0, 'status' => 'none', 'errors' => 0, ); if (empty($glob['name'])) { return; } $found = false; $config = array(); foreach ($list as $l) { if ($glob['name'] != $l['name']) { continue; } $found = true; $glob = array_merge($glob, $l); if (isset($l['config']) && !empty($l['config'])) { $config = $l['config']; } break; } if (!$found) { return; } $this->startImporter(); $this->imp->setMode(patI18n_Importer_Wombat::MODE_IMPORT); $ex = WBClass::create('WBNLS_Extractor_' . $glob['name']); if (empty($ex)) { return; } $ex->configure($config); $ex->setImporter($this->imp); $cnt = $ex->run(); $errors = $ex->getErrors(true); $glob['total'] = $cnt; $glob['status'] = 'ok'; if (count($errors)) { $glob['errors'] = count($errors); $glob['status'] = 'error'; } $this->tmpl->addGlobalVars($glob, 'EXTRACTOR_'); if ($this->tmpl->exists('extractor_errors_list_entry')) { $this->tmpl->addRows('extractor_errors_list_entry', $errors); } } private function startImporter() { $config = WBClass::create('WBConfig'); $config->load('locale'); $moduleConf = array( 'domain' => '', 'domains' => array('wombat', 'patForms'), ); $trans = $config->get('translators', array()); if (empty($trans) || !isset($trans['modules'])) { $trans['modules'] = array(); } foreach ($trans['modules'] as $mod) { if ('Wombat' != $mod['module']) { continue; } $moduleConf['domain'] = $mod['defaultdomain']; $moduleConf['domains'] = $mod['domains']; } $this->imp = patI18n::createImporter('Wombat', $moduleConf); } /** * List extractors * * @param array $list */ private function listExtractors($list) { if (!$this->tmpl->exists('extractor_list_entry')) { return; } if (empty($list)) { return; } $this->tmpl->addGlobalVar('extractor_list_count', count($list)); $this->tmpl->addRows('extractor_list_entry', $list); } /** * Get configured extrators * * @return array */ private function getExtrators() { $config = WBClass::create('WBConfig'); if (!$config->load('locale/extractor', true)) { return array(); } $available = $config->get('available', array()); $this->tmpl->addGlobalVar('extractor_list_count', count($available)); $list = array(); foreach ($available as $a) { if (is_array($a)) { $list[] = $a; continue; } $list[] = array( 'name' => $a, 'title' => $a, ); } return $list; } /** * receive output * * fetch output of this content component * * @return string */ public function getString() { return $this->tmpl->getParsedTemplate('snippet'); } /** * location of form config * * Return sub directory where form element definitions are located * * @return string folder */ protected function getFormConfigDir() { return 'locale/translator/form'; } /** * Get list of form elements * * Decide whether to use standard behaviour or build own form element list * * @param string $name * @return array */ protected function getFormElementList($name) { if (!in_array($name, array('edit', 'translate'))) { return parent::getFormElementList($name); } // use multiline element if text contains return character or some HTML tags $multiLine = false; $tags = false; if (strstr($this->current['msg'], "\n") || 100 < strlen($this->current['msg'])) { $multiLine = true; } if (strip_tags($this->current['msg']) != $this->current['msg']){ $multiLine = true; $tags = true; } $default = $this->current['msg']; if ('edit' == $name) { $des = 'msg'; $title = patI18n::dgettext('wombat', 'Message'); $desc = patI18n::dgettext('wombat', 'Translatable message'); } else { $des = 'translation'; $title = patI18n::dgettext('wombat', 'Translation'); $desc = patI18n::dgettext('wombat', 'Translated message'); // add old translation as default value $lang = substr($this->config['lang'], 0, 2); if (!empty($this->current['trans_' . $lang])) { $default = $this->current['trans_' . $lang]; } } $elements = array( $des => array( 'type' => 'String', 'attributes' => array( 'class' => $this->config['inputclassname'], 'title' => $title, 'label' => $title, 'description' => $desc, 'required' => 'yes', 'minlength' => 1, 'maxlength' => 256, 'default' => $default ) ) ); if ($multiLine) { $elements[$des]['type'] = 'Text'; $elements[$des]['attributes']['maxlength'] = 65535; $elements[$des]['attributes']['rows'] = 10; $elements[$des]['attributes']['allowedtags'] = '*'; $elements[$des]['attributes']['deniedtags'] = 'script,object,html,body,head,table,tr,th,td,font'; } // merge with config, if there is any $config = WBClass::create('WBConfig'); $merge = false; if ($config->load('locale/translator/form/edit', true)) { if ($multiLine) { $tmp = $config->get('form/elements/multiline', array()); if ($tags) { $tmp = $config->get('form/elements/wxml', $tmp); } } else { $tmp = $config->get('form/elements/singleline', array()); } if (isset($tmp['type'])) { $elements[$des]['type'] = $tmp['type']; } if (isset($tmp['attributes'])) { $elements[$des]['attributes'] = array_merge($elements[$des]['attributes'], $tmp['attributes']); } } return $elements; } /** * quote placeholders using entities * * @param string $txt * @param string $unquote * @return string */ private function quote($txt, $unquote = false) { $search = array('{', '}'); $repl = array('[[[', ']]]'); if ($unquote) { return str_replace($repl, $search, $txt); } return str_replace($search, $repl, $txt); } }