From 80424b29e00609bf837119fa810b5afdadf2b4e9 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Wed, 4 May 2016 18:27:28 +0200 Subject: Work on translations: templates and messages work --- modules-available/translation/page.inc.php | 651 ++++++++++++++++++++--------- 1 file changed, 449 insertions(+), 202 deletions(-) (limited to 'modules-available/translation/page.inc.php') diff --git a/modules-available/translation/page.inc.php b/modules-available/translation/page.inc.php index 3548f727..789500c5 100644 --- a/modules-available/translation/page.inc.php +++ b/modules-available/translation/page.inc.php @@ -1,18 +1,64 @@ builtInSections = array('template', 'messages', 'custom'); + } + + private function isValidSection($section) + { + return in_array($section, $this->builtInSections); + } + + private function loadCustomHandler($moduleName) + { + $path = 'modules/' . $moduleName . '/hooks/translation.inc.php'; + if (!file_exists($path)) + return false; + $HANDLER = array(); + require $path; + return $HANDLER; + } protected function doPreprocess() { @@ -22,26 +68,85 @@ class Page_Translation extends Page Message::addError('no-permission'); Util::redirect('?do=Main'); } + + // Set up variables + // Module to be edited + $moduleName = Request::any('module', false, 'string'); + if ($moduleName !== false) { + $this->module = Module::get($moduleName, true); + if ($this->module === false) { + Message::addError('main.no-such-module', $moduleName); + Util::redirect('?do=Translation'); + } elseif ($this->module->hasMissingDependencies()) { + Message::addError('main.module-missing-deps', $moduleName); + Util::redirect('?do=Translation'); + } + $this->customHandler = $this->loadCustomHandler($moduleName); + } + // Section + $sectionName = Request::any('section', false, 'string'); + if ($sectionName !== false) { + if (!$this->isValidSection($sectionName)) { + Message::addError('invalid-section', $sectionName); + if ($moduleName !== false) { + Util::redirect('?do=Translation&module=' . $moduleName); + } + Util::redirect('?do=Translation'); + } + } + // Section + $this->section = $sectionName; + // Subsection (being checked when used) + $this->subsection = Request::any('subsection', false, 'string'); + // LANG (verify if needed) + $this->destLang = Request::any('destlang', false, 'string'); if (Request::post('update')) { $this->updateJson(); Util::redirect('?do=Translation'); } - if (Request::post('delete')) { - $this->deleteTag(Request::post('path'), Request::post('delete')); - Util::redirect('?do=Translation'); // TODO: Ajax post for delete so we stay on the page + } + + private function ensureValidDestLanguage() + { + if (!in_array($this->destLang, Dictionary::getLanguages())) { + Message::addError('i18n-invalid-lang', $this->destLang); + Util::redirect('?do=Translation'); } - - $this->template = Request::get('template'); - $this->page = Request::get('page'); } protected function doRender() { $langs = Dictionary::getLanguages(true); + + // Overview (list of modules) + if ($this->module === false) { + $this->showModuleList(); + return; + } + + // One module - overview + if ($this->section === false) { + $this->showModule(); + return; + } + + // Edit template string + if ($this->section === 'template') { + $this->ensureValidDestLanguage(); + $this->showTemplateEdit(); + return; + } + + // Messugiz + if ($this->section === 'messages') { + $this->ensureValidDestLanguage(); + $this->showMessagesEdit(); + return; + } //load the page accordingly to the link - switch ($this->page) { + switch ($this->section) { case 'messages': //renders the message edit page Render::addTemplate('edit', array( @@ -79,56 +184,85 @@ class Page_Translation extends Page 'tags' => $this->buildTranslationTable('config-module') )); break; - case 'template': - $this->template = Util::safePath($this->template); - if ($this->template === false) { - Message::addError('invalid-path'); - Util::redirect('?do=Translation'); - } - //renders the tag edition page - Render::addTemplate('edit', array( - 'path' => 'modules/' . $this->template, - 'langs' => $langs, - 'tags' => $this->loadTemplateEditArray($this->template) - )); - break; - case 'templates': - //renders the template selection page - Render::addTemplate('template-list', array( - 'table' => $this->loadTemplatesList(), - )); - break; - case 'modules': - Render::addTemplate('module-list', array( - 'table' => $this->loadModuleTable() - )); - break; default: //renders main page with selection of what part to edit Render::addTemplate('_page'); } } - private function loadModuleTable(){ + private function showModuleList() + { $table = array(); - $modules = $this->loadModuleList(); + $modules = Module::getAll(); foreach ($modules as $module) { $msgs = $this->checkModuleTranslation($module); $table[] = array( - 'module' => $module, + 'module' => $module->getIdentifier(), + 'depfail' => $module->hasMissingDependencies(), 'status' => $msgs ); } sort($table); - return $table; + Render::addTemplate('module-list', array( + 'table' => $table + )); + } + + private function showModule() + { + $templateTags = $this->loadModuleTemplateTags(); + $data = array( + 'module' => $this->module->getIdentifier(), + 'moduleName' => $this->module->getDisplayName() + ); + $list = array(); + $data['tagcount'] = 0; + foreach ($templateTags as $templates) { + $list = array_merge($list, $templates); + $data['tagcount']++; + } + foreach (Dictionary::getLanguages(true) as $lang) { + list($missing, $unused) = $this->getModuleTemplateStatus($lang['cc'], $templateTags); + $data['langs'][] = array( + 'cc' => $lang['cc'], + 'name' => $lang['name'], + 'missing' => $missing, + 'unused' => $unused + ); + } + $data['templates'] = array(); + foreach (array_unique($list) as $template) { + $data['templates'][] = array('template' => $template); + } + Render::addTemplate('template-list', $data); + } + + private function showTemplateEdit() + { + Render::addTemplate('edit', array( + 'destlang' => $this->destLang, + 'tags' => $this->loadTemplateEditArray(), + 'module' => $this->module->getIdentifier(), + 'section' => $this->section + )); + } + + private function showMessagesEdit() + { + Render::addTemplate('edit', array( + 'destlang' => $this->destLang, + 'tags' => $this->loadMessagesEditArray(), + 'module' => $this->module->getIdentifier(), + 'section' => $this->section + )); } private function loadModuleEdit(){ $table = array(); - $tags = array_flip($this->loadModuleTags($this->module)); + $tags = array_flip($this->loadModuleTemplateTags($this->module)); foreach ($this->langs as $lang) { $tags = array_merge($tags, Dictionary::getArray($this->module,$lang['cc'])); } @@ -142,7 +276,7 @@ class Page_Translation extends Page 'placeholder' => 'TAG - ' . $lang['name'], 'translation' => $translations[$tag] ); - if(!in_array($tag, $this->loadModuleTags($this->module))) + if(!in_array($tag, $this->loadModuleTemplateTags($this->module))) $class = 'danger'; else if(!$translations[$tag]) $class = 'warning'; @@ -157,58 +291,105 @@ class Page_Translation extends Page return $table; } - private function loadModuleList(){ - // Return an array with the modules and the tags data - $list = array(); - $list = array_diff(scandir('modules/'), array('..', '.')); - return $list; + /** + * Get all tags used by templates of the given module. + * @param \Module $module module in question, false to use the one being edited + * @return array inde is tag, valie is array of templates using that tag + */ + private function loadModuleTemplateTags($module = false) + { + if ($module === false) { + $module = $this->module; + } + $tags = array(); + $path = 'modules/' . $module->getIdentifier() . '/templates'; + if (is_dir($path)) { + // Return an array with the module language tags + $objects = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)); + foreach ($objects as $name => $object) { + if (substr($name, -5) === '.html' && is_file($name)) { + $relTemplateName = substr($name, strlen($path), -5); + foreach ($this->getTagsFromTemplate($name) as $tag) { + $tags[$tag][] = $relTemplateName; + } + } + } + } + return $tags; } + + private function getTagsFromTemplate($templateFile) + { + //checks if the template is valid + if (!file_exists($templateFile)) { + Message::addError('invalid-template', $templateFile); + return false; + } - private function loadModuleTags($module){ - // Return an array with the module language tags - $path = "modules/" . $module . "templates/"; - $files = array_diff(scandir($path), array('..', '.')); - $tags = array(); - foreach ($files as $file) { - $content = file_get_contents($path . $file); - preg_match_all('/{{(lang_.*?)}}/s', $content, $matches); - if (isset($matches[1]) && is_array($matches[1])){ - $tags = array_merge($tags,array_unique($matches[1])); + //finds every mustache tag within the html template + $htmlTemplate = file_get_contents($templateFile); + preg_match_all('/{{(lang_.*?)}}/s', $htmlTemplate, $matches); + if (!isset($matches[1]) || !is_array($matches[1])) + return array(); + return array_unique($matches[1]); + } + + /** + * Get missing and unused counters for given module's templates. + * + * @param type $lang lang to use + * @param type $tags + * @param type $module + * @return array list(missingCount, unusedCount) + */ + private function getModuleTemplateStatus($lang, $tags = false, $module = false) + { + if ($module === false) { + $module = $this->module; + } + if ($tags === false) { + $tags = $this->loadModuleTemplateTags($module); + } + $globalTranslation = Dictionary::getArray('main', 'global-template-tags', $lang); + $translation = array_unique(array_merge(Dictionary::getArray($module->getIdentifier(), 'template-tags', $lang), $globalTranslation)); + $matches = 0; + $unused = 0; + $expected = count($tags); + foreach ($translation as $key => $value) { + if(!isset($tags[$key])) { + if (!in_array($key, $globalTranslation)) { + $unused++; + } + } else { + $matches++; } + } - return array_unique($tags); + $missing = $expected - $matches; + return array($missing, $unused); } - private function checkModuleTranslation($module){ - $tags = $this->loadModuleTags($module); - $translation = array(); + private function checkModuleTranslation($module) + { + $tags = $this->loadModuleTemplateTags($module); $msgs = ''; - foreach ($this->langs as $key => $lang) { - $translation = Dictionary::getArray($module,$lang['cc']); - $matches = 0; - $unused = 0; - $expected = count($tags); - foreach ($translation as $key => $value) { - if(!in_array($key, $tags)) - $unused ++; - else if(!empty($value)) - $matches ++; + foreach (Dictionary::getLanguages() as $lang) { + list($missing, $unused) = $this->getModuleTemplateStatus($lang, $tags, $module); - } - - $diff = $expected - $matches; $msg = ""; - if ($diff > 0) - $msg .= $diff . " JSON tag(s) are missing"; - if ($diff > 0 && $unused > 0) - $msg .= "
"; - if ($unused > 0) - $msg .= $unused . " JSON tag(s) are not being used"; - if(!empty($msg)) - $msgs .= "
{$lang['name']}: $msg
"; - } - if(empty($msgs)) + if ($missing > 0) { + $msg .= " [$missing JSON tag(s) are missing] "; + } + if ($unused > 0) { + $msg .= " [$unused JSON tag(s) are not being used] "; + } + if(!empty($msg)) { + $msgs .= "
$lang
$msg
"; + } + } + if(empty($msgs)) { $msgs = 'OK'; + } return $msgs; } /** @@ -271,13 +452,13 @@ class Page_Translation extends Page * Finds and returns all PHP files of slxadmin * @return array of all php files */ - private function listPhp() + private function getModulePhpFiles() { $php = array(); - $dir = '.'; + $dir = 'modules/' . $this->module->getIdentifier(); $objects = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)); foreach ($objects as $name => $object) { - if (substr($name, -4) === '.php') { + if (substr($name, -4) === '.php' && is_file($name)) { $php[] = $name; } } @@ -330,16 +511,83 @@ class Page_Translation extends Page } /** - * Get array to pass to edit page with all the tags and translations for the given template + * Get array to pass to edit page with all the tags and translations. + * * @param string $path the template's path - * @return array the information about the JSON tags + * @return array structure to pass to the tags list in the edit template */ - private function loadTemplateEditArray($path) + private function loadTemplateEditArray() { - $tags = $this->loadTemplateTags($path); + $tags = $this->loadModuleTemplateTags(); if ($tags === false) return false; - return $this->buildTranslationTable("modules/" . $path, $tags); + $table = $this->buildTranslationTable('template-tags', array_keys($tags), true); + $global = Dictionary::getArray($this->module->getIdentifier(), 'global-template-tags', $this->destLang); + foreach ($table as &$entry) { + if (empty($entry['translation']) && isset($global[$entry['tag']])) { + $entry['placeholder'] = $global[$entry['tag']]; + } + if (!isset($tags[$entry['tag']])) + continue; + $entry['notes'] = implode('
', $tags[$entry['tag']]); + } + return $table; + } + + /** + * Get array to pass to edit page with all the message ids. + * + * @param string $path the template's path + * @return array structure to pass to the tags list in the edit template + */ + private function loadMessagesEditArray() + { + // TODO: Scan for uses in other modules, handle module.id syntax + $tags = $this->loadTagsFromPhp('/Message\s*::\s*add\w+\s*\(\s*[\'"]([^\'"]*)[\'"]\s*(\)|\,.*)/i'); + $table = $this->buildTranslationTable('messages', array_keys($tags), true); + foreach ($table as &$entry) { + if (isset($tags[$entry['tag']]) && is_string($tags[$entry['tag']])) { + $entry['notes'] = 'Params: ' . $this->countMessageParams($tags[$entry['tag']]); + } + } + return $table; + } + + private function countMessageParams($str) + { + error_log($str); + $quote = false; + $escape = false; + $count = 0; + $len = strlen($str); + $depth = 0; + for ($i = 0; $i < $len; ++$i) { + $char = $str{$i}; + if ($escape) { + $escape = false; + continue; + } + if ($quote === false) { + if ($char === ',') { + if ($depth === 0) { + $count++; + } + } elseif ($char === '"' || $char === "'") { + $quote = $char; + } elseif ($char === '{' || $char === '(' || $char === '[') { + $depth++; + } elseif ($char === '}' || $char === ')' || $char === ']') { + $depth--; + } + } else { + if ($char === $quote) { + $quote = false; + } elseif ($char === '\\') { + $escape = true; + } + } + } + return $count; } /** @@ -364,24 +612,13 @@ class Page_Translation extends Page return array_unique($matches[1]); } - /** - * Load array of tags and translations of all messages - * @return array the information about the JSON tags - */ - private function loadMessageEditArray() - { - $tags = $this->loadTagsFromPhp('/Message\s*::\s*add\w+\s*\(\s*[\'"](.*?)[\'"]\s*[\)\,]/i'); - if ($tags === false) - return false; - return $this->buildTranslationTable('messages', $tags); - } - /** * Load array of tags and translations of all strings found in the php files. * @return array the information about the JSON tags */ private function loadHardcodedStringEditArray() { + // TODO: Return changed $tags = $this->loadTagsFromPhp('/Dictionary\s*::\s*translate\s*\(\s*[\'"]([^\'"]*?)[\'"]\s*\)/i'); if ($tags === false) return false; @@ -397,65 +634,70 @@ class Page_Translation extends Page private function loadTagsFromPhp($regexp) { // Get all php files, so we can find all strings that need to be translated - $php = $this->listPhp(); + $php = $this->getModulePhpFiles(); $tags = array(); // Now find all tags in all php files. Only works for literal usage, not something like $foo = 'bar'; Dictionary::translate($foo); foreach ($php as $file) { $content = @file_get_contents($file); - if ($content === false || preg_match_all($regexp, $content, $out) < 1) + if ($content === false || preg_match_all($regexp, $content, $out, PREG_SET_ORDER) < 1) continue; - foreach ($out[1] as $id) { - $tags[$id] = true; + foreach ($out as $set) { + $tags[$set[1]] = isset($set[2]) ? $set[2] : true; } } - return array_keys($tags); + return $tags; } - private function buildTranslationTable($path, $requiredTags = false) + private function buildTranslationTable($file, $requiredTags = false, $findAlreadyTranslated = false) { - // All languages - $langArray = Dictionary::getLanguages(); - $tags = array(); if ($requiredTags !== false) { foreach ($requiredTags as $tagName) { $tags[$tagName] = array('tag' => $tagName); - foreach ($langArray as $lang) { - $tags[$tagName]['langs'][$lang]['lang'] = $lang; - $tags[$tagName]['missing'] = count($langArray); - } } } - // Finds every JSON tag within the JSON language files - foreach ($langArray as $lang) { - $jsonTags = Dictionary::getArray($path, $lang, true); - if (!is_array($jsonTags)) - continue; + // Finds every tag within the JSON language file + $jsonTags = Dictionary::getArray($this->module->getIdentifier(), $file, $this->destLang); + if (is_array($jsonTags)) { foreach ($jsonTags as $tag => $translation) { - $tags[$tag]['langs'][$lang]['translation'] = $translation; - $tags[$tag]['langs'][$lang]['lang'] = $lang; - if (strpos($translation, "\n") !== false) - $tags[$tag]['langs'][$lang]['big'] = true; + $tags[$tag]['translation'] = $translation; + if (strpos($translation, "\n") !== false) { + $tags[$tag]['big'] = true; + } $tags[$tag]['tag'] = $tag; - if (!isset($tags[$tag]['missing'])) - $tags[$tag]['missing'] = 0; - if (!empty($translation)) - $tags[$tag]['missing'] --; } } - // Fill the blanks - foreach ($langArray as $lang) { - foreach (array_keys($tags) as $tagName) { - if (!isset($tags[$tagName]['langs'][$lang])) - $tags[$tagName]['langs'][$lang]['lang'] = $lang; + if ($findAlreadyTranslated) { + $srcLangs = array_merge(array(LANG), array('en'), Dictionary::getLanguages()); + $srcLangs = array_unique($srcLangs); + $key = array_search($this->destLang, $srcLangs); + if ($key !== false) { + unset($srcLangs[$key]); + } + foreach ($srcLangs as $lang) { + $otherLang = Dictionary::getArray($this->module->getIdentifier(), $file, $lang); + if (!is_array($otherLang)) + continue; + $missing = false; + foreach (array_keys($tags) as $tag) { + if (isset($tags[$tag]['samplelang'])) + continue; + if (!isset($otherLang[$tag])) { + $missing = true; + } else { + $tags[$tag]['samplelang'] = $lang; + $tags[$tag]['sampletext'] = $otherLang[$tag]; + } + } + if (!$missing) + break; } } - // Finally remove $lang from the keys so mustache will iterate over them via {{#..}} + $tagid = 0; foreach ($tags as &$tag) { - $tag['langs'] = array_values($tag['langs']); - if ($requiredTags !== false) - $tag['class'] = $this->getTagColor($tag['missing']); + $tag['tagid'] = $tagid++; } + // Finally remove $lang from the keys so mustache will iterate over them via {{#..}} return array_values($tags); } @@ -477,91 +719,96 @@ class Page_Translation extends Page //if it's ok don't change the class return ''; } + + private function getJsonFile() + { + $prefix = 'modules/' . $this->module->getIdentifier() . '/lang/' . $this->destLang; + // File + if ($this->section === 'messages') { + return $prefix . '/messages.json'; + } + if ($this->section === 'template') { + return $prefix . '/template-tags.json'; + } + // Custom submodule + if ($this->section === 'custom') { + if ($this->customHandler === false || !isset($this->customHandler['subsections'])) { + Message::addError('no-custom-handlers'); + Util::redirect('?do=Translation'); + } + if (!in_array($this->subsection, $this->customHandler['subsections'], true)) { + Message::addError('invalid-custom-handler', $this->subsection); + Util::redirect('?do=Translation'); + } + return $prefix . '/' . $this->subsection; + } + Message::addError('invalid-section', $this->section); + Util::redirect('?do=Translation'); + } /** * Updates a JSON file with it's new tags or/and tags values */ private function updateJson() { - $langArray = Dictionary::getLanguages(); - foreach ($langArray as $lang) { - $json[$lang] = array(); + $this->ensureValidDestLanguage(); + if ($this->module === false) { + Message::addError('main.no-module-given'); + Util::redirect('?do=Translation'); } + $file = $this->getJsonFile(); + + $data = array(); //find the tag requests to change the file foreach ($_POST as $key => $value) { - $str = explode('#', $key, 3); - if (count($str) !== 3 || $str[0] !== 'lang') - continue; - $lang = $str[1]; - $tag = trim($str[2]); - if (!isset($json[$lang])) { - Message::addWarning('i18n-invalid-lang', $lang); + $str = explode('#!#', $key, 2); + if (count($str) !== 2) continue; - } - if (empty($tag)) { - Message::addWarning('i18n-empty-tag'); - continue; - } - $value = trim($value); - if ($tag !== 'newtag') { + if ($str[0] === 'lang') { + $tag = trim($str[1]); + if (empty($tag)) { + Message::addWarning('i18n-empty-tag'); + continue; + } if (empty($value)) { - unset($json[$lang][$tag]); + unset($data[$tag]); } else { - $json[$lang][$tag] = $value; + $data[$tag] = $value; } - } else { - if (!empty($value)) // TODO: Error message if new tag's name collides with existing - $json[$lang][$_REQUEST['newtag']] = $value; } } - - // JSON_PRETTY_PRINT is only available starting with php 5.4.0.... Use upgradephp's json_encode - require_once('inc/up_json_encode.php'); + + $translation = Request::post('new-text', array(), 'array'); + foreach (Request::post('new-id', array(), 'array') as $k => $tag) { + if (empty($translation[$k])) + continue; + $data[(string)$tag] = (string)$translation[$k]; + } //saves the new values on the file - foreach ($json as $key => $array) { - $path = Util::safePath('lang/' . $key . '/' . Request::post('path') . '.json'); - if ($path === false) { - Message::addError('invalid-path'); - Util::redirect('?do=Translation'); + $path = dirname($file); + if (!is_dir($path)) { + mkdir($path, 0755, true); + } + + if (empty($data)) { + if (file_exists($file)) { + unlink($file); } - @mkdir(dirname($path), 0755, true); - ksort($array); // Sort by key, so the diff on the output is cleaner - $json = up_json_encode($array, JSON_PRETTY_PRINT); // Also for better diffability of the json files, we pretty print + } else { + // JSON_PRETTY_PRINT is only available starting with php 5.4.0.... Use upgradephp's json_encode + require_once('inc/up_json_encode.php'); + ksort($data); // Sort by key, so the diff on the output is cleaner + $json = up_json_encode($data, JSON_PRETTY_PRINT); // Also for better diffability of the json files, we pretty print //exits the function in case the action was unsuccessful - if (@file_put_contents($path, $json) === false) { + if (file_put_contents($file, $json) === false) { Message::addError('invalid-template'); return; } } - Message::addSuccess('updated-tags'); - } - /** - * Delete a specific JSON tag from a JSON files - * @var string the JSON's file path - * @var the JSON tag to be deleted - * @return boolean if the action was not successful - */ - private function deleteTag($path, $tag) - { - // JSON_PRETTY_PRINT is only available starting with php 5.4.0.... Use upgradephp's json_encode - require_once('inc/up_json_encode.php'); - - //delete the tag from every language file - $langArray = Dictionary::getLanguages(); - foreach ($langArray as $lang) { - $json = Dictionary::getArray($path, $lang); - unset($json[$tag]); - $result = file_put_contents('lang/' . $lang . '/' . $path . '.json', up_json_encode($json, JSON_PRETTY_PRINT)); - //add warning and exit in case the action was unsuccessful - if ($result === false) { - Message::addWarning('unsuccessful-action'); - return false; - } - } - Message::addSuccess('deleted-tag'); + Message::addSuccess('updated-tags'); } /** -- cgit v1.2.3-55-g7522