* @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.5.5 * @package WB * @subpackage content */ class WBContent_Locale_Translator extends WBContent { /** * Parameter list * * requiredgroup: user group for uses that are allowed to translate messages * requiredgroupmanager: user group for uses that may do anything * goto: page to display * action: selected action, one of: overview, list, edit, rm, translate, extract and orphancheck * * * @var array */ protected $config = array( 'requiredgroup' => 'nls-translator', 'requiredlangperm' => 0, 'requiredgroupmanager' => 'contenteditor', 'action' => 'list', 'goto' => 0, 'limit' => 50, ); /** * table access * @var WBDatasource_Table */ private $table; /** * I18n message importer * @var patI18n_Importer */ private $imp; /** * temporary varibable holding current dataset * @var array */ private $current = array(); /** * Current Translation Target Language * * @var string */ private $currentLang = 'C'; /** * Origin Language * * @var string */ private $originLang = 'C'; /** * Pager Count Column Options * @var array */ protected $pagerCountColumn = array(); /** * Translation Module Config * @var array */ private $moduleConfig; /** * 2nd constructor */ protected function init() { $this->config['usefilter'] = 1; $this->config['searchfields'] = array('msg'); $this->cachable = false; /** @var WBFormProcessor_Fiddler_Translator */ $fiddler = WBClass::create('WBFormProcessor_Fiddler_Translator'); $this->moduleConfig = $fiddler->getModuleConfig(); $this->currentLang = $fiddler->getDefaultLanguage(); $this->addFilterFiddler($fiddler); // don't automatically translate anything and handle event by myself $this->table = WBClass::create('WBDatasource_Table'); $this->table->switchTranslation(false); $this->table->switchEventTrigger(false); } /** * Run Component * * Actually do stuff :-) * * @return array parameter list */ public function run() { if (!$this->isUserInGroup($this->config['requiredgroup'])) { $this->loadTemplates('anon'); return $this->config; } // add origin language as template var /** @var WBConfig */ $config = WBClass::create('WBConfig'); $config->load('locale'); $this->originLang = $config->get('locale/languages/origin', 'C'); $this->tmpl->addGlobalVar('locale_origin', $this->originLang); // additional search fields $this->config['searchfields'][] = $this->table->getIdentifier('nlsmsg'); $this->currentLang = $this->req->get('filter_lang', $this->currentLang); if ('C' != $this->currentLang) { $this->config['searchfields'][] = 'trans_' . $this->currentLang; } /** @var WBDictionary_Language */ $dict = WBClass::create('WBDictionary_Language'); $dict->load($this->currentLang); $this->tmpl->addGlobalVars($dict->get(), 'dict_current_'); $dict->load($this->originLang); $this->tmpl->addGlobalVars($dict->get(), 'dict_origin_'); $dict->load(patI18n::getLocale(patI18n::LOCALE_TYPE_LANG)); $this->tmpl->addGlobalVars($dict->get(), 'dict_locale_'); $translatable = $this->setupLanguagePerm(); $this->tmpl->addGlobalVar('lang_current', $this->currentLang); $this->addConfigAsGlobalVars(); $this->initFilter(); $this->runAction(); return $this->config; } /** * Check Permissions for Langauge * * @return bool */ private function setupLanguagePerm() { if ($this->isUserInGroup($this->config['requiredgroupmanager'])) { $this->tmpl->addGlobalVar('user_is_manager', 1); $this->tmpl->addGlobalVar('lang_translatable', 1); return true; } // limit actions if (!in_array($this->config['action'], array('list', 'translate'))) { $this->config['action'] = 'list'; } $langs = $this->table->get('nlslangtranslator', null, $this->user->getId()); foreach ($langs as $l) { if ($l['lang'] == $this->currentLang) { $this->tmpl->addGlobalVar('lang_translatable', 1); return true; } } $this->config['action'] = 'list'; $this->tmpl->addGlobalVar('lang_translatable', 0); return false; } /** * Run Component Action * */ public function runAction() { switch ($this->config['action']) { case 'list': $this->displayList(); 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; } } /** * Display List of Records * * In case there is no template named "list_entry", it will be loaded and action will be set accordingly */ protected function displayList() { if (!$this->tmpl->exists('list_entry')) { $this->config['action'] = 'list'; $this->tmpl->addGlobalVar('config_action', 'list'); $this->loadTemplates('list'); } $clause = $this->filter->getClause(); $options = $this->filter->getOptions(); $this->tweakFilter($clause, $options); $id = __CLASS__ . '-' . $this->part . '-'. patI18n_Module_Wombat::TABLE; $pager = $this->table->getPager($id, patI18n_Module_Wombat::TABLE, $foreign, $clause, $options); $pager->setCountColumn($this->pagerCountColumn); $this->tmpl->addGlobalVars($pager->browse($this->config['goto']), 'pager_'); $this->tmpl->addGlobalVars($pager->getCount(), 'list_'); $this->tmpl->addRows('list_entry', $this->prepareListEntry($pager->get())); } /** * Tweak Table Filter On Demand * * Implement this method to mange with filter clause and options * * @param array * @param option */ protected function tweakFilter(&$clause, &$option) { // need the language first $lang = null; foreach ($clause as &$c) { if ('lang' != $c['field']) { continue; } $lang = $c['value']; $c = array( 'type' => 'native', 'clause' => '1' ); break; } // now we are going for all other fields foreach ($clause as &$c) { switch ($c['field']) { // message length case 'msglength': if (-1 == $c['value']) { $c = array( 'type' => 'native', 'clause' => '1' ); } else if (0 < $c['value']) { $c = array( 'type' => 'native', 'clause' => sprintf('char_length(msg) > %s', $c['value']) ); } else { $c['value'] = abs($c['value']); $c = array( 'type' => 'native', 'clause' => sprintf('char_length(msg) < %s', $c['value']) ); } break; case 'orphan': if (0 < $c['value']) { $c['value'] = 1; } else { $c = array( 'type' => 'native', 'clause' => '1' ); } break; case 'translationstatus': switch ($c['value']) { case 'any': $c = array( 'type' => 'native', 'clause' => '1' ); break; case 'untranslated': $c = array( 'field' => 'trans_' . $lang, 'relation' => 'null', 'value' => 1 ); break; case 'identical': $c = array( 'field' => 'trans_' . $lang, 'relation' => 'eq', 'foreign' => 'nlsmsg', 'value' => 'msg', 'valuetype' => 'foreign' ); break; case 'translated': $c = array( 'field' => 'trans_' . $lang, 'relation' => 'null', 'value' => 0 ); break; case 'fuzzy': $clause[] = array( 'field' => 'trans_' . $lang, 'relation' => 'null', 'value' => 0 ); $c = array( 'field' => 'changed_' . $lang, 'relation' => 'lt', 'foreign' => 'nlsmsg', 'value' => 'changed', 'valuetype' => 'foreign' ); break; case 'autotranslated': $clause[] = array( 'field' => 'trans_' . $lang, 'relation' => 'null', 'value' => 0 ); $c = array( 'field' => 'changed_' . $lang, 'relation' => 'eq', 'foreign' => 'nlsmsg', 'value' => 'changed', 'valuetype' => 'foreign' ); break; default: break; } break; case 'xnamespace': if ('*' == $c['value']) { $c = array( 'type' => 'native', 'clause' => '1' ); } break; default: break; } } $option = array( 'limit' => 50, // default limit 'order' => array( array( 'field' => 'created', 'asc' => 0 ) ), 'column' => array('singular', 'msg', 'domain', 'created', 'orphan','xnamespace','xid') ); if ($this->config['limit'] && intval($this->config['limit']) > 0) { $options['limit'] = intval($this->config['limit']); } // select translations if (!empty($lang) && $this->moduleConfig['clang'] != $lang) { $option['column'][] = array( 'field' => 'trans_' . $lang, 'as' => 'translation' ); $option['column'][] = array( 'field' => 'changed_' . $lang, 'as' => 'translation_changed' ); $option['column'][] = array( 'field' => 'min_' . $lang, 'as' => 'translation_min' ); } } /** * Prepare List Entries 4 Template * * @param array * @return array */ protected function prepareListEntry($list) { foreach ($list as &$l) { $l['msg'] = $this->quote($l['msg']); if (!isset($l['translation'])) { $l['translation'] = ''; } else { $l['translation'] = $this->quote($l['translation']); } } return $list; } /** * edit original message */ private function editMsg() { if (!$this->load()) { return $this->displayList(); } $this->current['msg'] = $this->quote($this->current['msg']); $this->tmpl->addGlobalVars($this->current); $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->useSuperPlaceholder($this->quote($values['msg'], true)), 'changed' => gmdate('Y-m-d H:i:s') ); $this->save($save); $this->displayList(); return false; } /** * remove message and translations */ private function rmMsg() { if (!$this->load()) { return $this->displayList(); } $this->table->delete('nlsmsg', $this->current['id']); return $this->displayList(); } /** * translate message */ private function translateMsg() { if (!$this->load()) { return $this->displayList(); } $trans = 'trans_' . $this->currentLang; $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->currentLang; if (empty($values['translation'])) { $values['translation'] = null; } else { $values['translation'] = $this->useSuperPlaceholder($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->displayList(); return false; } /** * Use Placeholders * * Replace self URL-String with matching placeholders * * @param string * @return string */ private function useSuperPlaceholder($in) { $self = '[[PROTOCOL]]://[[SERVER]][[SELF]]'; $text = WBString::replaceSuperPlaceholders($self); return str_replace($text, $self, $in); } /** * 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' ); $moduleConf = $this->getConfig4ModuleWombat(); if (!empty($moduleConf['domain'])) { // use default domain $clause[] = array( 'field' => 'domain', 'value' => $moduleConf['domain'], ); } else { // otherwise don't use other domains $clause[] = array( 'field' => 'domain', 'relation' => 'not_in', 'value' => $moduleConf['domains'], ); } $this->table->save('nlsmsg', null, $save, $clause); $this->startImporter(); $this->imp->setMode(patI18n_Importer_Wombat::MODE_ORPHANCHECK); foreach ($list as &$l) { /** @var WBNLS_Extractor */ $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); } /** * Count Orphan Messages * * Count messages marked as orphans */ 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']); $this->current['msg_trans'] = ''; $lang = patI18n::getLocale(patI18n::LOCALE_TYPE_LANG); if ($this->currentLang == $lang || $this->originLang == $lang) { return true; } if (isset($this->current['trans_' . $lang])) { $this->current['msg_trans'] = $this->current['trans_' . $lang]; } 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); $pUrl = $this->table->getIdentifier('url'); $lang = $this->currentLang; $eData = array( 'id' => $this->current['id'], 'lang' => $lang, 'type' => '', $pUrl => $this->current[$pUrl], 'path' => $this->current['path'], ); if (isset($save['msg'])) { $eData['body'] = $save['msg']; $eData['type'] = 'saved'; } else { $eData['body'] = $save['trans_' . $lang]; $eData['type'] = 'translated'; } WBClass::load('WBEvent'); WBEvent::trigger('nls:translator:' . $eData['type'] . ':' . $lang, 'Saved NLS message {ID} ({LANG})', $eData); 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); } } /** * Start Importer Module * * @see $imp */ private function startImporter() { $this->imp = patI18n::createImporter('Wombat', $this->moduleConfig); } /** * Get Module Settings for Wombat Module * * Extract translator config for Wombat module * Show list of extactors in template * * @see $locale * @return array */ private function getConfig4ModuleWombat() { $moduleConf = array( 'domain' => '', 'domains' => array('wombat', 'patForms'), ); $config = WBClass::create('WBConfig'); $config->load('locale'); $trans = $config->get('translators/modules', array()); if (empty($trans)) { return $moduleConf; } foreach ($trans as $mod) { if ('Wombat' != $mod['module']) { continue; } $moduleConf['domain'] = $mod['defaultdomain']; $moduleConf['domains'] = $mod['domains']; } return $moduleConf; } /** * List Extractors * * Show list of extactors in template * * @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 * * Fetch list from config * * @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; } /** * 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) { // other forms if (!in_array($name, array('list', '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->currentLang, 0, 2); if (!empty($this->current['trans_' . $lang])) { $default = $this->current['trans_' . $lang]; } } $elements = array( $des => array( 'type' => 'String', 'attributes' => array( 'class' => 'form-control', '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,style,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']); } } $this->tmpl->addGlobalVar('form_element_type', strtolower($elements[$des]['type'])); return $elements; } /** * Quote Placeholders Using Entities * * Replace curly braces using brackets and vice vers * * @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); } }