From dbc0d9614421e064cc62aacf116ebb783c83f2f3 Mon Sep 17 00:00:00 2001 From: Jonathan Bauer Date: Fri, 1 Apr 2016 16:50:13 +0200 Subject: [merge] merging c3sl / fr - initial commit --- modules/translation/module.inc.php | 597 +++++++++++++++++++++++++++++++++++++ 1 file changed, 597 insertions(+) create mode 100644 modules/translation/module.inc.php (limited to 'modules/translation/module.inc.php') diff --git a/modules/translation/module.inc.php b/modules/translation/module.inc.php new file mode 100644 index 00000000..3548f727 --- /dev/null +++ b/modules/translation/module.inc.php @@ -0,0 +1,597 @@ +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 + } + + $this->template = Request::get('template'); + $this->page = Request::get('page'); + } + + protected function doRender() + { + $langs = Dictionary::getLanguages(true); + + //load the page accordingly to the link + switch ($this->page) { + case 'messages': + //renders the message edit page + Render::addTemplate('edit', array( + 'path' => 'messages', + 'langs' => $langs, + 'tags' => $this->loadMessageEditArray() + )); + break; + case 'hardcoded': + //renders the hardcoded messages edit page + Render::addTemplate('edit', array( + 'path' => 'messages-hardcoded', + 'langs' => $langs, + 'tags' => $this->loadHardcodedStringEditArray() + )); + break; + case 'settings': + //renders the settings related edit page + Render::addTemplate('edit', array( + 'path' => 'cat_setting', + 'langs' => $langs, + 'tags' => $this->loadCategoriesArray() + )); + Render::addTemplate('edit', array( + 'path' => 'setting', + 'langs' => $langs, + 'tags' => $this->loadSettingsArray() + )); + break; + case 'config-module': + //renders the hardcoded messages edit page + Render::addTemplate('edit', array( + 'path' => 'config-module', + 'langs' => $langs, + '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(){ + $table = array(); + + $modules = $this->loadModuleList(); + + foreach ($modules as $module) { + $msgs = $this->checkModuleTranslation($module); + $table[] = array( + 'module' => $module, + 'status' => $msgs + ); + } + + sort($table); + return $table; + } + + private function loadModuleEdit(){ + $table = array(); + $tags = array_flip($this->loadModuleTags($this->module)); + foreach ($this->langs as $lang) { + $tags = array_merge($tags, Dictionary::getArray($this->module,$lang['cc'])); + } + foreach ($tags as $tag => $value) { + $langArray = array(); + $class = ''; + foreach ($this->langs as $lang) { + $translations = Dictionary::getArray($this->module,$lang['cc']); + $langArray[] = array( + 'lang' => $lang['cc'], + 'placeholder' => 'TAG - ' . $lang['name'], + 'translation' => $translations[$tag] + ); + if(!in_array($tag, $this->loadModuleTags($this->module))) + $class = 'danger'; + else if(!$translations[$tag]) + $class = 'warning'; + } + $table[] = array( + 'tag' => $tag, + 'class' => $class, + 'langs' => $langArray + ); + } + + 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; + } + + 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])); + } + } + return array_unique($tags); + } + + private function checkModuleTranslation($module){ + $tags = $this->loadModuleTags($module); + $translation = array(); + $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 ++; + + } + + $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)) + $msgs = 'OK'; + return $msgs; + } + /** + * Load the main table with all the website's templates and it's informations + * @return array with the templates' information + */ + private function loadTemplatesList() + { + $table = array(); + + //loads every template + $files = $this->listTemplates(); + $langs = Dictionary::getLanguages(true); + + //checks the JSON tags from every language + foreach ($files as $file) { + $tags = $this->loadTemplateTags($file['path']); + // Don't list templates without lang tags + if (empty($tags)) + continue; + $msgs = ''; + foreach ($langs as $lang) { + $msg = $this->checkJson($file['path'], $lang['cc'], $tags); + if (!empty($msg)) + $msgs .= "
{$lang['name']}:$msg
"; + } + if (empty($msgs)) + $msgs = 'OK'; + $table[] = array( + 'template' => $file['name'], + 'link' => $file['name'], + 'status' => $msgs + ); + } + sort($table); + return $table; + } + + /** + * Finds and returns all the website's templates + * @return array + */ + private function listTemplates() + { + $files = array(); + $dir = 'modules/'; + $objects = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)); + foreach ($objects as $name => $object) { + if (substr($name, -5) === '.html') { + $files[] = array( + 'path' => substr($name, 0, -5), + 'name' => substr($name, strlen($dir), -5) + ); + } + } + return $files; + } + + /** + * Finds and returns all PHP files of slxadmin + * @return array of all php files + */ + private function listPhp() + { + $php = array(); + $dir = '.'; + $objects = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)); + foreach ($objects as $name => $object) { + if (substr($name, -4) === '.php') { + $php[] = $name; + } + } + return $php; + } + + /** + * Checks the JSON tags from a template + * @param string the template's path + * @param string the selected language + * @param string tags that should be in the json file + * @return string|boolean the information about the JSON tags, false if template has no lang-tags + */ + private function checkJson($path, $lang, $expectedTags) + { + //if there was not a valid template's path + if (!$path) { + return "Translation missing"; + } + // How many tags do we expect in the translation + $htmlCount = count($expectedTags); + + //initialize the count variables + $matchCount = 0; + $unusedCount = 0; + + //loads the JSON tags and count the matches + $json = Dictionary::getArray(substr($path, strlen("modules/")), $lang); + //return print_r($json) . "\nvs\n" . print_r($expectedTags); + foreach ($json as $key => $value) { + if (!in_array($key, $expectedTags)) { + $unusedCount++; + } else if (!empty($value)) { + $matchCount++; + } + } + $diff = $htmlCount - $matchCount; + + if ($diff == 0 && $unusedCount == 0) + return ''; + //build the return string + $str = ""; + if ($diff > 0) + $str .= $diff . " JSON tag(s) are missing"; + if ($diff > 0 && $unusedCount > 0) + $str .= "
"; + if ($unusedCount > 0) + $str .= $unusedCount . " JSON tag(s) are not being used"; + return $str; + } + + /** + * Get array to pass to edit page with all the tags and translations for the given template + * @param string $path the template's path + * @return array the information about the JSON tags + */ + private function loadTemplateEditArray($path) + { + $tags = $this->loadTemplateTags($path); + if ($tags === false) + return false; + return $this->buildTranslationTable("modules/" . $path, $tags); + } + + /** + * Load array of tags used in given template. + * @param string $path the path of the template, relative to templates/, without .html extension. + * @return array all tags in template + */ + private function loadTemplateTags($path) + { + $templateFile = "$path.html"; + //checks if the template is valid + if (!file_exists($templateFile)) { + Message::addError('invalid-template', $templateFile); + return false; + } + + //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]); + } + + /** + * 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() + { + $tags = $this->loadTagsFromPhp('/Dictionary\s*::\s*translate\s*\(\s*[\'"]([^\'"]*?)[\'"]\s*\)/i'); + if ($tags === false) + return false; + return $this->buildTranslationTable('messages-hardcoded', $tags); + } + + /** + * Load array of tags used in all the php files, by given regexp. Capture group 1 should return + * the exact tag name. + * @param string $regexp regular expression matching all tags in capture group 1 + * @return array of all tags found + */ + private function loadTagsFromPhp($regexp) + { + // Get all php files, so we can find all strings that need to be translated + $php = $this->listPhp(); + $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) + continue; + foreach ($out[1] as $id) { + $tags[$id] = true; + } + } + return array_keys($tags); + } + + private function buildTranslationTable($path, $requiredTags = 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; + 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]['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; + } + } + // Finally remove $lang from the keys so mustache will iterate over them via {{#..}} + foreach ($tags as &$tag) { + $tag['langs'] = array_values($tag['langs']); + if ($requiredTags !== false) + $tag['class'] = $this->getTagColor($tag['missing']); + } + return array_values($tags); + } + + /** + * Change the color of the table line according to the tag status + * @param string the JSON's path + * @param string the selected tag + * @return string the css class of the line + */ + private function getTagColor($missingCount) + { + //return danger in case the tag is not found in the template + if ($missingCount < 0) + return 'danger'; + + //return warning in case at least one of the tag's values is empty + if ($missingCount > 0) + return 'warning'; + //if it's ok don't change the class + return ''; + } + + /** + * 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(); + } + + //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); + continue; + } + if (empty($tag)) { + Message::addWarning('i18n-empty-tag'); + continue; + } + $value = trim($value); + if ($tag !== 'newtag') { + if (empty($value)) { + unset($json[$lang][$tag]); + } else { + $json[$lang][$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'); + + //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'); + } + @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 + //exits the function in case the action was unsuccessful + if (@file_put_contents($path, $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'); + } + + /** + * Load all settings categories for editing. + * + * @return array + */ + private function loadCategoriesArray() + { + $want = array(); + $res = Database::simpleQuery("SELECT catid FROM cat_setting ORDER BY catid ASC"); + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + $want[] = 'cat_' . $row['catid']; + } + return $this->buildTranslationTable('settings/cat_setting', $want); + } + + /** + * Load all settings categories for editing. + * + * @return array + */ + private function loadSettingsArray() + { + $want = array(); + $res = Database::simpleQuery("SELECT setting FROM setting ORDER BY setting ASC"); + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + $want[] = $row['setting']; + } + return $this->buildTranslationTable('settings/setting', $want); + } + +} -- cgit v1.2.3-55-g7522