summaryrefslogblamecommitdiffstats
path: root/modules-available/translation/page.inc.php
blob: 5f915782558a6d927c25490816a18886b49ae75b (plain) (tree)
1
2
3
4
5
6
7
8
9

     

                                


                                                                
                                   
 
 
           
                                                                                  
           






















                                                                                  
                                                                      








                                     
                                                                                                           

         
                                                              



                                                                  
                                                                      

                                                                                
                                   




















                                                                                                                        
                                    
                               
         




                                                                   

                                                        










                                                                                                                                                   
 

                                         
                             
 
                                          
                                                                

                                                   







                                                                                      
                                                  

                                                                                           
                                                  


                                                                                     






                                                                                           
                                 
                         
                                                      
                 



                                                                                
 

                                              
                                           
                 





                                                                                
                                          
                 

         

                                     

                                              
                                                   





















                                                         






                                                         
 






                                                         




                                                         
                 
                
                                   
         
 
                                            
         

                                 
                                            

                                               
                                                                             
                                         

                                                                               




                                                 






                                                         

                                                            

                                                                       
                   
                                                                



                                             

                                           

                                                  

                                             

                                          
                                        





                                                                          
                                         

                                                       
                                                                                 











                                                                                                             
                                                                     



                                                                              




                                                                          
                                    

                                                      
                                                                                            


                                                                   
                                                                                                                                  







                                                        
                                                                



                                                                      
















                                                                                                                              






                                                                          
                                                                                                                                  








                                                                 
        
                                                 
         
                                                  











                                                                              
                                                                                         

                                                                   




                                                                                                                                          








                                                          




                                                      
                                                                                   









                                                                     
                                                                                   



                                                                     

         










                                                                                   










                                                                                   











                                                                                   

                                                              
                                                                                       

                                                                           
           
                                                                           
         
                                       





                                                                             



                                                                                     



                                 
         








                                                                
                                                                                      

                                  
                                                                          
         
                                       

                                                




                                                                                                                                  
                                                                                                                                           
                                   
                           
                                                                  

                                                          
                                                                                  
                                                                 
                                                                                

                                                    
                         
                 
                                                                                                                                 
                                                                  
         



                                             
                                                      
           
                                                                         
         
                                       

                                                





                                                                                                                                                               





                                                           
                                                                                            







                                                    
 
                                                        
         
                                        




                                                             
                                                                                 




                                                                  







                                                                            
                                                       
           
                                                                       
         










                                                                                          
         
        
                                                                  





                                                                             
 










                                                                             

                                                                             
                                                                                                 
                                                          
           
                                                                                                         
         
                                                                                                       
         






































































                                                                                                                                                           

           


                                                                                  
          

                                                                                    
                                                                                                
                                                                                               
                                                                                                 
                                                    
           
                                                                                                                                          
         
                                       

                                                





                                                                                                                 

                             






                                                            
                                                 
                                                                       

                                                  
                                
                                                                                                    


                                                            





                                                
 
                                                                             
         


                                                                     
                           
                                                               


                                                                                                                           


                                                                                         
 
                                  
                                           
                                                               

                                          
                                                                     

                                          
                                                                                                                                                                         


                                  
                                     
                 

                             
 
           


                                                       
           
                                                                           
         



                                                                     
                                         




                                                                                                
                         
                 
                            
         
 
           

                                                             
                                                               
                                          
           
                                                                 
         
                                                                                         


           

                                                                             
                                                                                
           
                                                       
         
                                                      
                                                                                                
                                                                                                               













                                                                                            

                                                                                
                                                       
         
                                                     

                                                                                           










                                                                                                                   




                              


                                                                   

                                                                                
                                                     

                                                    
                                                                                       
         
 
                                                           

                                                        
                                                                                           
         



                                                                   

                                                                                
                                                     

                                                                     



                                                                                       
                                                                                                










                                                                                            
                                                             
         





                                               
                                         
                                                                    



                                                

                                                                          




                                                           

















                                                                                    

                         


                                                                                                     
                              


           









                                                                                             
                                                      

                                                                
                                                                                                         
           
                                                                                               

                                                                                           
                                                                                                                                                  
                                           
                                                            
                                               
                                         






                                                                                                
                                                








                                                                                

                         
                             

         

                                                
                                                                          


                                                                                                                    
                                                                                                                             
         
                                

                                                                                       
                 

                                                                                    

                                                                                                         





                                                                                                                           
                         
                                                  
                 
                                             

                                                                                                       
                 




                                                                                                
                           
                                          
                                                 








                                                                                                    
                         
                 
                                                                                                      
                                           
         
 
           




                                                                                           
                                                                                        
           
                                                                                 
         





                                                                                                            













                                                                                     
         
        
                                              








                                                                                                   


                                                        


                                                            

                                                  

                                                                                                                                                   
                         


                                                                                   
                         

                                                                                       

                                                                     
                                   
         
 

                                                                    
           
                                           
         

                                                 
                                                             
                                          
                 


                                             
 
                                                          




                                                                      
                                         




                                                     
                         
                 

                                                                                   
                                                                   


                                                                       
 
                                                  

                                       
                                                 




                                                 
                         
                        
                                                                                          
                                                                                                                                        
                                                                                
                                                                        
                                                                             
                                       

                         
 
                                                    
         
 
 
<?php

use JetBrains\PhpStorm\NoReturn;

/**
 * The pages where you can administrate the website translations
 */
class Page_Translation extends Page
{

	/**
	 * @var string holds the target template, or subsection for custom section
	 */
	private $subsection = false;
	
	/**
	 * @var \Module module being edited, false for module list
	 */
	private $module = false;

	/**
	 * @var string used to choose which page to load (mode of operation)
	 */
	private $section = false;
	
	/**
	 * @var string[] list of sections that are handled directly by this module
	 */
	private $builtInSections;
	
	/**
	 * @var array Custom module handler, if any, false otherwise
	 */
	private $customHandler = false;
	
	/**
	 * @var string Language being handled (if any in current step)
	 */
	private $destLang = false;
	
	/*
	 * 
	 */
	
	public function __construct()
	{
		$this->builtInSections = array('template', 'messages', 'module', 'menucategory', 'custom');
	}
	
	private function isValidSection(string $section): bool
	{
		return in_array($section, $this->builtInSections);
	}
	
	private function loadCustomHandler(string $moduleName): ?array
	{
		$path = 'modules/' . $moduleName . '/hooks/translation.inc.php';
		$HANDLER = array();
		if (file_exists($path)) {
			require $path;
		}
		$backup = $HANDLER;
		foreach (glob('modules/*/hooks/translation-global.inc.php', GLOB_NOSORT) as $path) {
			$HANDLER = array();
			require $path;
			if (empty($HANDLER['subsections']))
				continue;
			foreach ($HANDLER['subsections'] as $sub) {
				$suf = '';
				while (isset($backup['subsections']) && in_array($sub . $suf, $backup['subsections'])) {
					$suf = mt_rand();
				}
				$backup['subsections'][] = $sub . $suf;
				if (isset($HANDLER['grep_' . $sub])) {
					$backup['grep_' . $sub . $suf] = $HANDLER['grep_' . $sub];
				}
			}
		}
		if (empty($backup))
			return null;
		return $backup;
	}
	
	/**
	 * Redirect to the closest matching page, as extracted from
	 * get/post parameters.
	 */
	#[NoReturn]
	private function redirect(int $level = 99): void
	{
		$params = array('do' => 'translation');
		if ($level > 0 && $this->module !== false) {
			$params['module'] = $this->module->getIdentifier();
		}
		if ($level > 1 && $this->section !== false && $this->destLang !== false && in_array($this->destLang, Dictionary::getLanguages())) {
			$params['section'] = $this->section;
			$params['destlang'] = $this->destLang;
		}
		Util::redirect('?' . http_build_query($params));
	}

	protected function doPreprocess()
	{
		User::load();

		if (!User::isLoggedIn()) {
			Message::addError('main.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);
				$this->redirect();
			} elseif ($this->module->hasMissingDependencies()) {
				Message::addError('main.module-missing-deps', $moduleName);
				$this->redirect();
			}
			$this->customHandler = $this->loadCustomHandler($moduleName);
		}
		if ($this->module !== false) {
			// Section
			$sectionName = Request::any('section', false, 'string');
			if ($sectionName !== false) {
				if (!$this->isValidSection($sectionName)) {
					Message::addError('invalid-section', $sectionName);
					$this->redirect();
				}
			}
			$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();
			$this->redirect(1);
		}
	}
	
	private function ensureValidDestLanguage()
	{
		if (!in_array($this->destLang, Dictionary::getLanguages())) {
			Message::addError('i18n-invalid-lang', $this->destLang);
			$this->redirect();
		}
	}

	protected function doRender()
	{
		// Overview (list of modules)
		if ($this->module === false) {
			$this->showListOfModules();
			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;
		}
		
		// Module
		if ($this->section === 'module') {
			$this->ensureValidDestLanguage();
			$this->showModuleEdit();
			return;
		}

		// Menu Category
		if ($this->section === 'menucategory') {
			$this->ensureValidDestLanguage();
			$this->showMenuCategoryEdit();
			return;
		}

		// Custom
		if ($this->section === 'custom') {
			$this->ensureValidDestLanguage();
			$this->showCustomEdit();
			return;
		}
		
		$this->redirect(1);
	}

	private function showListOfModules()
	{
		$table = array();

		$modules = Module::getAll();
		
		foreach ($modules as $module) {
			$msgs = $this->buildStatusStringForOverview($module);
			$table[] = array(
				'module' => $module->getIdentifier(),
				'depfail' => $module->hasMissingDependencies(),
				'status' => $msgs
			);
		}

		sort($table);
		Render::addTemplate('module-list', array(
			'table' => $table
		));
	}
	
	private function showModule()
	{
		// Heading
		Render::addTemplate('module-heading', array(
			'module' => $this->module->getIdentifier(),
			'moduleName' => $this->module->getDisplayName()
		));
		Render::openTag('div', array('class' => 'row'));
		// Templates
		$this->showModuleTemplates();
		// Messages
		$this->showModuleMessages();
		// Other/hardcoded strings
		$this->showModuleStrings();
		// Menu categories
		$this->showModuleMenuCategories();
		// Dict::translateFile calls
		$this->getModuleOtherFiles();
		// Module specific
		$this->showModuleCustom();
		Render::closeTag('div');
	}
	
	private function showModuleTemplates()
	{
		$templateTags = $this->loadUsedTemplateTags();
		$data = array('module' => $this->module->getIdentifier());
		$templateNames = array();
		$data['tagcount'] = 0;
		foreach ($templateTags as $templates) {
			$templateNames = array_merge($templateNames, $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($templateNames) as $template) {
			$data['templates'][] = array('template' => $template);
		}
		Render::addTemplate('template-list', $data);
	}
	
	private function showModuleMessages()
	{
		$messageTags = $this->loadUsedMessageTags();
		$data = array('module' => $this->module->getIdentifier());
		$phpFiles = array();
		$data['messagecount'] = 0;
		foreach ($messageTags as $templates) {
			$phpFiles = array_merge($phpFiles, array_keys($templates['files']));
			$data['messagecount']++;
		}
		foreach (Dictionary::getLanguages(true) as $lang) {
			list($missing, $unused) = $this->getModuleTranslationStatus($lang['cc'], 'messages', false, $messageTags);
			$data['langs'][] = array(
				'cc' => $lang['cc'],
				'name' => $lang['name'],
				'missing' => $missing,
				'unused' => $unused
			);
		}
		$data['files'] = array();
		foreach (array_unique($phpFiles) as $template) {
			$data['files'][] = array('file' => $template);
		}
		Render::addTemplate('message-list', $data);
	}
	
	private function showModuleStrings()
	{
		$moduleTags = $this->loadUsedModuleTags();
		$data = array('module' => $this->module->getIdentifier());
		$data['tagcount'] = count($moduleTags);
		foreach (Dictionary::getLanguages(true) as $lang) {
			list($missing, $unused) = $this->getModuleTranslationStatus($lang['cc'], 'module', true, $moduleTags);
			$data['langs'][] = array(
				'cc' => $lang['cc'],
				'name' => $lang['name'],
				'missing' => $missing,
				'unused' => $unused
			);
		}
		Render::addTemplate('string-list', $data);
	}

	private function showModuleMenuCategories()
	{
		$moduleTags = $this->loadUsedMenuCategories();
		$data = array('module' => $this->module->getIdentifier());
		$data['tagcount'] = count($moduleTags);
		foreach (Dictionary::getLanguages(true) as $lang) {
			list($missing, $unused) = $this->getModuleTranslationStatus($lang['cc'], 'categories', true, $moduleTags);
			$data['langs'][] = array(
				'cc' => $lang['cc'],
				'name' => $lang['name'],
				'missing' => $missing,
				'unused' => $unused
			);
		}
		Render::addTemplate('menu-category-list', $data);
	}
	
	private function showModuleCustom(): void
	{
		if ($this->customHandler === null)
			return;
		foreach ($this->customHandler['subsections'] as $subsection) {
			$this->showModuleCustomSubsection($subsection);
		}
	}
	
	private function showModuleCustomSubsection($subsection)
	{
		$moduleTags = $this->loadUsedCustomTags($subsection);
		$data = array(
			'subsection' => $subsection,
			'module' => $this->module->getIdentifier(),
			'tagcount' => $moduleTags === false ? '???' : count($moduleTags),
		);
		foreach (Dictionary::getLanguages(true) as $lang) {
			if ($moduleTags !== false) {
				list($missing, $unused) = $this->getModuleTranslationStatus($lang['cc'], $subsection, false, $moduleTags);
			} else {
				$missing = $unused = '???';
			}
			$data['langs'][] = array(
				'cc' => $lang['cc'],
				'name' => $lang['name'],
				'missing' => $missing,
				'unused' => $unused
			);
		}
		Render::addTemplate('custom-list', $data);
	}

	private function showTemplateEdit()
	{
		Render::addTemplate('edit', array(
			'destlang' => $this->destLang,
			'language' => Dictionary::getLanguageName($this->destLang),
			'tags'     => $this->loadTemplateEditArray(),
			'module'   => $this->module->getIdentifier(),
			'section'  => $this->section
		));
	}

	private function showMessagesEdit()
	{
		Render::addTemplate('edit', array(
			'destlang' => $this->destLang,
			'language' => Dictionary::getLanguageName($this->destLang),
			'tags'     => $this->loadMessagesEditArray(),
			'module'   => $this->module->getIdentifier(),
			'section'  => $this->section
		));
	}

	private function showModuleEdit()
	{
		Render::addTemplate('edit', array(
			'destlang' => $this->destLang,
			'language' => Dictionary::getLanguageName($this->destLang),
			'tags'     => $this->loadModuleEditArray(),
			'module'   => $this->module->getIdentifier(),
			'section'  => $this->section
		));
	}

	private function showMenuCategoryEdit()
	{
		Render::addTemplate('edit', array(
			'destlang' => $this->destLang,
			'language' => Dictionary::getLanguageName($this->destLang),
			'tags'     => $this->loadMenuCategoryEditArray(),
			'module'   => $this->module->getIdentifier(),
			'section'  => $this->section
		));
	}

	private function showCustomEdit()
	{
		Render::addTemplate('edit', array(
			'destlang' => $this->destLang,
			'language' => Dictionary::getLanguageName($this->destLang),
			'tags'     => $this->loadCustomEditArray(),
			'module'   => $this->module->getIdentifier(),
			'section'  => $this->section,
			'subsection' => $this->subsection
		));
	}

	/**
	 * Get all tags used by templates of the given module.
	 * @param \Module $module module in question, null to use the one being edited.
	 *
	 * @return array of array(tag => array of templates using that tag)
	 */
	private function loadUsedTemplateTags(Module $module = null): array
	{
		if ($module === null) {
			$module = $this->module;
		}
		$tags = array();
		$path = 'modules/' . $module->getIdentifier() . '/templates';
		if (is_dir($path)) {
			// Return an array with the module language tags
			foreach ($this->getAllFiles($path, '.html') as $name) {
				$relTemplateName = substr($name, strlen($path), -5);
				foreach ($this->getTagsFromTemplate($name) as $tag) {
					$tags[$tag][] = $relTemplateName;
				}
			}
		}
		return $tags;
	}

	/**
	 * Get all message tags of the given module.
	 * Returns array indexed by tag, value is
	 * array(
	 *	    'data' => rest of arguments to message
	 *     'files' => array(filename => occurencecount, ...)
	 * )
	 *
	 * @param \Module $module module in question, null to use the one being edited
	 * @return array see above
	 */
	private function loadUsedMessageTags(Module $module = null): array
	{
		if ($module === null) {
			$module = $this->module;
		}
		$allFiles = $this->getAllFiles('modules', '.php');
		if ($module->getIdentifier() === 'main') {
			$allFiles = array_merge($allFiles, $this->getAllFiles('apis', '.php'), $this->getAllFiles('inc', '.php'));
			$allFiles[] = 'index.php';
		}
		$full = $this->loadTagsFromPhp(['/Message\s*::\s*add\w+\s*\(\s*[\'"](?<tag>[^\'"\.]*\.[^\'"]*)[\'"]\s*(?<data>\)|\,.*)/i'],
			$allFiles);
		$tags = [];
		// Filter out tags that don't refer to this module
		foreach ($full as $tag) {
			$p = explode('.', $tag['tag'], 2);
			// Figure out if this is a message from this module or not
			if ($p[0] === $module->getIdentifier()) {
				// Direct reference to this module via module.id
				$tag['tag'] = $p[1];
				$tags[$p[1]] = $tag;
			}
		}
		return $this->loadTagsFromPhp(['/Message\s*::\s*add\w+\s*\(\s*[\'"](?<tag>[^\'"\.]*)[\'"]\s*(?<data>\)|\,.*)/i'],
			$this->getModulePhpFiles($module), $tags);
	}

	/**
	 * Get all module tags used/required.
	 *
	 * @return array (<tagname> => (bool)required)
	 */
	private function loadUsedModuleTags(Module $module = null): array
	{
		if ($module === null) {
			$module = $this->module;
		}
		$emod = preg_quote($module->getIdentifier(), '/');
		$tags = $this->loadTagsFromPhp([
			'/Dictionary\s*::\s*translate\s*\(\s*[\'"](?<tag>[^\'"\.]*)[\'"]\s*[\),]/i',
			'/Dictionary\s*::\s*translateFile\s*\(\s*[\'"]module[\'"]\s*,\s*[\'"](?<tag>[^\'"\.]*)[\'"]\s*[\),]/i',
			'/Dictionary\s*::\s*translateFileModule\s*\(\s*[\'"]'.$emod.'[\'"]\s*,\s*[\'"]module[\'"]\s*,\s*[\'"](?<tag>[^\'"\.]*)[\'"]\s*[\),]/i',
		],
			$this->getModulePhpFiles($module));
		foreach ($tags as &$tag) {
			$tag = true;
		}
		unset($tag);
		// Fixup special tags
		if (!file_exists('modules/' . $module->getIdentifier() . '/page.inc.php')) {
			unset($tags['module_name']);
			unset($tags['page_title']);
		} else {
			$tags['module_name'] = true;
			$tags['page_title'] = false;
		}
		return $tags;
	}

	private function loadUsedMenuCategories(): array
	{
		$module = $this->module;
		$skip = strlen($module->getIdentifier()) + 1;
		$match = $module->getIdentifier() . '.';
		$want = array();
		foreach (Module::getAll() as $module) {
			$cat = $module->getCategory();
			if ($cat !== null && substr($cat, 0, $skip) === $match) {
				$want[substr($cat, $skip)] = true;
			}
		}
		return $want;
	}

	/**
	 * Return list of existing tags needing translation. This calls
	 * the function defined by the module. which is expected to return
	 * an array serving as a list, where the KEYS are the tags expected,
	 * the value of each entry can be false if the tag is optional.
	 *
	 * @param string $subsection Name of subsection
	 * @return ?array List of tags as KEYS of array
	 */
	private function loadUsedCustomTags(string $subsection): ?array
	{
		if (isset($this->customHandler['grep_'.$subsection])) {
			return $this->customHandler['grep_' . $subsection]($this->module);
		}
		$byFile = $this->getDictTranslateFileArray($this->module);
		if (isset($byFile[$subsection])) {
			foreach ($byFile[$subsection] as &$tag) {
				$tag = true;
			}
			return $byFile[$subsection];
		}
		return null;
	}
	
	private function getTagsFromTemplate(string $templateFile)
	{
		//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]);
	}
	
	/**
	 * Get missing and unused counters for given module's templates.
	 *
	 * @param string $lang lang to use
	 * @param array $tags Array of tags, where the tag names are the keys
	 * @param Module $module the module to work with, defaults to the currently edited module
	 * @return array{0: int, 1 :int} (missing, unused)
	 */
	private function getModuleTemplateStatus(string $lang, array $tags, Module $module = null): array
	{
		return $this->getModuleTranslationStatus($lang, 'template-tags', true, $tags, $module);
	}

	private function getDictTranslateFileArray(Module $module): array
	{
		$tags = $this->loadTagsFromPhp(
			['/Dictionary\s*::\s*translateFile\s*\(\s*[\'"](?<json>[^\'".]*)[\'"]\s*,\s*[\'"](?<tag>[^\'".]*)[\'"]\s*[),]/i'],
			$this->getModulePhpFiles($module));
		$byFile = [];
		foreach ($tags as $tag) {
			if ($tag['json'] === 'messages' || $tag['json'] === 'module' || $tag['json'] === 'permissions' || $tag['json'] === 'template-tags')
				continue;
			if (!isset($byFile[$tag['json']])) {
				$byFile[$tag['json']] = [];
			}
			$byFile[$tag['json']][$tag['tag']] = $tag;
		}
		return $byFile;
	}

	/**  @return array{0: int, 1 :int} (missing, unused) */
	private function getModuleOtherFileStatus(string $lang, Module $module = null, array $byFile = null): array
	{
		if ($module === null) {
			$module = $this->module;
		}
		if ($byFile === null) {
			$byFile = $this->getDictTranslateFileArray($module);
		}
		$missing = $unused = 0;
		foreach ($byFile as $file => $list) {
			$translation = Dictionary::getArray($module->getIdentifier(), $file, $lang);
			foreach ($list as $tag) {
				$tag = $tag['tag'];
				if (isset($translation[$tag])) {
					unset($translation[$tag]);
				} else {
					$missing++;
				}
			}
			$unused += count($translation);
		}
		return [$missing, $unused];
	}

	/**  @return array{missingCount: int, unusedCount :int} */
	private function getModuleOtherFiles(): void
	{
		$module = $this->module;
		$this->renderDictFileOneLang($module);
	}

	private function renderDictFileOneLang(Module $module): void
	{
		$byFile = $this->getDictTranslateFileArray($module);
		foreach ($byFile as $file => $list) {
			$data = [
				'subsection' => $file,
				'module' => $this->module->getIdentifier(),
				'tagcount' => count($list),
			];
			foreach (Dictionary::getLanguages(true) as $lang) {
				list($missing, $unused) = $this->getModuleOtherFileStatus($lang['cc'], $module, $byFile);
				$data['langs'][] = array(
					'cc' => $lang['cc'],
					'name' => $lang['name'],
					'missing' => $missing,
					'unused' => $unused
				);
			}
			Render::addTemplate('custom-list', $data);
		}
	}
	
	/**
	 * Get missing and unused counters for given translation unit.
	 * This is a more general version of the getModuleTemplateStatus function,
	 * which is special since it uses fallback to global translations.
	 *
	 * @param string $lang lang cc to use
	 * @param string $file the name of the translation file to load for checking
	 * @param bool $fallback whether to check the global-tags of the main module as fallback
	 * @param array $tags list of tags that are expected to exist. Tags are the array keys!
	 * @param Module $module the module to work with, defaults to the currently edited module
	 * @return array [missingCount, unusedCount]
	 */
	private function getModuleTranslationStatus(string $lang, string $file, bool $fallback, array $tags, Module $module = null): array
	{
		if ($module === null) {
			$module = $this->module;
		}
		if ($fallback) {
			$globalTranslation = Dictionary::getArray('main', 'global-tags', $lang);
		} else {
			$globalTranslation = array();
		}
		$translation = Dictionary::getArray($module->getIdentifier(), $file, $lang) + $globalTranslation;
		$matches = 0;
		$unused = 0;
		$expected = 0;
		foreach ($tags as $v) {
			if ($v !== false) {
				$expected++;
			}
		}
		foreach (array_keys($translation) as $key) {
			if(!isset($tags[$key])) {
				if (!isset($globalTranslation[$key])) {
					$unused++;
				}
			} else {
				// Only if mandatory, since $expected is also calculated accordingly
				if ($tags[$key] !== false) {
					$matches++;
				}
			}

		}
		$missing = $expected - $matches;
		return array($missing, $unused);
	}

	private function buildStatusStringForOverview(Module $module): string
	{
		$templateTags = $this->loadUsedTemplateTags($module);
		$messageTags = $this->loadUsedMessageTags($module);
		$moduleTags = $this->loadUsedModuleTags($module);
		$msgs = '';
		foreach (Dictionary::getLanguages() as $lang) {
			list($m1, $u1) = $this->getModuleTemplateStatus($lang, $templateTags, $module);
			list($m2, $u2) = $this->getModuleTranslationStatus($lang, 'messages', true, $messageTags, $module);
			list($m3, $u3) = $this->getModuleTranslationStatus($lang, 'module', true, $moduleTags, $module);
			list($m4, $u4) = $this->getModuleOtherFileStatus($lang, $module);
			$missing = $m1 + $m2 + $m3 + $m4;
			$unused = $u1 + $u2 + $u3 + $u4;

			$msg = "";
			if ($missing > 0) {
				$msg .= " [$missing missing] ";
			}
			if ($unused > 0) {
				$msg .= " [$unused not being used] ";
			}
			if(!empty($msg)) {
				$msgs .= '<div><div class="pull-left">' . Dictionary::getFlagHtml(false, $lang) . '</div>' . $msg . '<div class="clearfix"></div></div>';
			}
		}
		if(empty($msgs)) {
			$msgs = 'OK';
		}
		return $msgs;
	}

	/**
	 * Finds and returns all PHP files of slxadmin.
	 *
	 * @return array of all php file names
	 */
	private function getAllFiles(string $dir, string $extension): array
	{
		$php = array();
		$extLen = -strlen($extension);
		foreach (scandir($dir, SCANDIR_SORT_NONE) as $name) {
			if ($name === '.' || $name === '..')
				continue;
			$name = $dir . '/' . $name;
			if (substr($name, $extLen) === $extension && is_file($name)) {
				$php[] = $name;
			} else if (is_dir($name)) {
				$php = array_merge($php, $this->getAllFiles($name, $extension));
			}
		}
		return $php;
	}

	/**
	 * Finds and returns all PHP files of current module.
	 *
	 * @param Module $module Module to get the php files of
	 * @return array of php file names
	 */
	private function getModulePhpFiles(Module $module): array
	{
		return $this->getAllFiles('modules/' . $module->getIdentifier(), '.php');
	}

	/**
	 * Get array to pass to edit page with all the tags and translations.
	 *
	 * @return array structure to pass to the tags list in the edit template
	 */
	private function loadTemplateEditArray(): array
	{
		$tags = $this->loadUsedTemplateTags();
		$table = $this->buildTranslationTable('template-tags', array_keys($tags), true);
		$global = Dictionary::getArray($this->module->getIdentifier(), 'global-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('<br>', $tags[$entry['tag']]);
		}
		return $table;
	}
	
	/**
	 * Get array to pass to edit page with all the message ids.
	 *
	 * @return array structure to pass to the tags list in the edit template
	 */
	private function loadMessagesEditArray(): array
	{
		$tags = $this->loadUsedMessageTags();
		$table = $this->buildTranslationTable('messages', array_keys($tags), true);
		foreach ($table as &$entry) {
			if (!isset($tags[$entry['tag']]))
				continue;
			$tag =& $tags[$entry['tag']];
			// Add tag information
			if (isset($tag['data']) && is_string($tag['data'])) {
				$entry['notes'] = '<b>Params: ' . $this->countMessageParams($tag['data']) . '</b>';
			} else {
				$entry['notes'] = '';
			}
			foreach ($tag['files'] as $file => $count) {
				$entry['notes'] .= '<br>' . htmlspecialchars($file) . ': ' . $count . '×';
			}
		}
		return $table;
	}
	
	/**
	 * Get array to pass to edit page with all the message ids.
	 *
	 * @return array structure to pass to the tags list in the edit template
	 */
	private function loadModuleEditArray(): array
	{
		$tags = $this->loadUsedModuleTags();
		return $this->buildTranslationTable('module', array_keys($tags), true);
	}

	private function loadMenuCategoryEditArray(): array
	{
		$tags = $this->loadUsedMenuCategories();
		return $this->buildTranslationTable('categories', array_keys($tags), true);
	}
	
	/**
	 * Get array to pass to edit page with all the message ids.
	 *
	 * @return array structure to pass to the tags list in the edit template
	 */
	private function loadCustomEditArray(): array
	{
		$tags = $this->loadUsedCustomTags($this->subsection);
		if ($tags === null) {
			Message::addError('invalid-custom-handler', $this->subsection);
			$this->redirect(1);
		}
		return $this->buildTranslationTable($this->subsection, array_keys($tags), true);
	}
	
	/**
	 * Quick and dirty method to count the parameters of a message/translate invocation.
	 * Expects the rest of an invocation, so e.g. addMessage('foo-foo', 'hi'); becomes
	 * , 'hi'); or addMessage('foo'); becomes just );
	 * This obviously fails if the call is spread over multiple lines.
	 *
	 * @param string $str the partial method call
	 * @return int number of arguments to the method, minus the message id
	 */
	private function countMessageParams(string $str): int
	{
		$quote = false;
		$escape = false;
		$count = 0;
		$len = strlen($str);
		$depth = 0;
		for ($i = 0; $i < $len; ++$i) {
			$char = $str[$i];
			// Last char was backslash? Ignore this char
			if ($escape) {
				$escape = false;
				continue;
			}
			// We're inside quotes, watch for end or backslash
			if ($quote !== false) {
				if ($char === $quote) {
					$quote = false;
				} elseif ($char === '\\') {
					$escape = true;
				}
				continue;
			}
			// We're not inside quotes
			// Check if we have a parameter delimiter
			if ($char === ',') {
				// Check we're not in a nested method call
				if ($depth === 0) {
					$count++; // Increase parameter counter
				}
			} elseif ($char === '"' || $char === "'") {
				// Start of string
				$quote = $char;
			} elseif ($char === '{' || $char === '(' || $char === '[') {
				// Nested method etc.
				$depth++;
			} elseif ($char === '}' || $char === ')' || $char === ']') {
				// End nested method
				$depth--;
			}
		}
		// QnD special case for Message::add* using true as second param to add "go to" link.
		if (preg_match('/^\s*,\s*true\b/i', $str))
			return $count - 1;
		return $count;
	}

	/**
	 * Load array of tags used in all the php files, by given regexp.
	 * The capture group containing the tag name must be named tag, which
	 * can be achieved by using (?<tag>..). All other capture groups will
	 * be returned in the resulting array.
	 * The return value is an array indexed by tag, and the value for each tag is of type
	 * array('captureX' => captureX, 'captureY' => captureY, 'files' => array(
	 *	    file1 => count,
	 *	    fileN => count
	 * )).
	 *
	 * @param string[] $regexp regular expressions
	 * @param string[] $files list of files to scan
	 * @param string[] $tags existing tag array to append to
	 * @return array of all tags found, where the tag is the key, and the value is as described above
	 */
	private function loadTagsFromPhp(array $regexps, array $files, array $tags = []): array
	{
		// Get all php files, so we can find all strings that need to be translated
		// Now find all tags in all php files. Only works for literal usage, not something like $foo = 'bar'; Dictionary::translate($foo);
		foreach ($files as $file) {
			$content = file_get_contents($file);
			if ($content === false)
				continue;
			$out = [];
			foreach ($regexps as $regexp) {
				$tmp = [];
				if (preg_match_all($regexp, $content, $tmp, PREG_SET_ORDER) < 1)
					continue;
				$out = array_merge($out, $tmp);
			}
			foreach ($out as $set) {
				if (!isset($tags[$set['tag']])) {
					$tags[$set['tag']] = $set;
					$tags[$set['tag']]['files'] = array();
				}
				if (isset($tags[$set['tag']]['files'][$file])) {
					$tags[$set['tag']]['files'][$file]++;
				} else {
					$tags[$set['tag']]['files'][$file] = 1;
				}
			}
		}
		return $tags;
	}

	/**
	 * @param string $file Source dictionary
	 * @param string[] $requiredTags Tags that are considered required
	 * @param bool $findAlreadyTranslated If true, try to find a translation for this string in another language
	 * @return array numeric array suitable for passing to mustache
	 */
	private function buildTranslationTable(string $file, array $requiredTags, bool $findAlreadyTranslated = false): array
	{
		$tags = array();
		foreach ($requiredTags as $tagName) {
			$tags[$tagName] = array('tag' => $tagName, 'required' => true);
		}
		// Sort here, so all tags known to be used are in alphabetical order
		ksort($tags);
		// Finds every tag within the JSON language file
		$jsonTags = Dictionary::getArray($this->module->getIdentifier(), $file, $this->destLang);
		// Sort these separately so unused tags will be at the bottom of the list, but still ordered alphabetically
		ksort($jsonTags);
		foreach ($jsonTags as $tag => $translation) {
			$tags[$tag]['translation'] = $translation;
			if (strpos($translation, "\n") !== false) {
				$tags[$tag]['big'] = true;
			}
			$tags[$tag]['tag'] = $tag;
		}
		if ($findAlreadyTranslated) {
			// For each tag, include a translated string from another language as reference
			$this->findTranslationSamples($file, $tags);
		}
		if ($file === 'template-tags' || $file === 'module') {
			$globals = Dictionary::getArray('main', 'global-tags', $this->destLang);
		} else {
			$globals = array();
		}
		$tagid = 0;
		foreach ($tags as &$tag) {
			$tag['tagid'] = $tagid++;
			// We have a list of required tags, so mark those that are missing or unused
			if (!isset($tag['required'])) {
				$tag['unused'] = true;
			} elseif (!isset($tag['translation']) && !isset($globals[$tag['tag']])) {
				$tag['missing'] = true;
			}
			if (isset($globals[$tag['tag']])) {
				$tag['isglobal'] = true;
				$tag['placeholder'] = $globals[$tag['tag']];
			}
		}
		// Finally remove tagname from the keys so mustache will iterate over them via {{#..}}
		return array_values($tags);
	}

	/**
	 * Finds translation samples for the given tags in the given file, looking in all
	 * languages except the one currently being translated to. Prefers the language the
	 * user selected, then english, then everything else.
	 *
	 * @param string $file translation unit
	 * @param array $tags list of tags, formatted as used in buildTranslationTable()
	 */
	private function findTranslationSamples(string $file, array &$tags): void
	{
		$srcLangs = array_unique(array_merge(array(LANG), array('en'), Dictionary::getLanguages()));
		if (($key = array_search($this->destLang, $srcLangs)) !== false) {
			unset($srcLangs[$key]);
		}
		foreach ($srcLangs as $lang) {
			$otherLang = Dictionary::getArray($this->module->getIdentifier(), $file, $lang);
			$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;
		}
	}
	
	private function getJsonFile(): string
	{
		$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';
		}
		if ($this->section === 'module') {
			return $prefix . '/module.json';
		}
		if ($this->section === 'menucategory') {
			return $prefix . '/categories.json';
		}
		// Custom submodule
		if ($this->section === 'custom') {
			if (isset($this->customHandler['subsections']) && in_array($this->subsection, $this->customHandler['subsections'], true)) {
				return $prefix . '/' . $this->subsection . '.json';
			}
			$byFile = $this->getDictTranslateFileArray($this->module);
			if (isset($byFile[$this->subsection])) {
				return $prefix . '/' . $this->subsection . '.json';
			}
			Message::addError('invalid-custom-handler', $this->subsection);
			$this->redirect(1);
		}
		Message::addError('invalid-section', $this->section);
		$this->redirect(1);
	}

	/**
	 * Updates a JSON file with it's new tags or/and tags values
	 */
	private function updateJson(): void
	{
		$this->ensureValidDestLanguage();
		if ($this->module === false) {
			Message::addError('no-module-given');
			$this->redirect();
		}
		$file = $this->getJsonFile();
		
		$data = array();

		//find the tag requests to change the file
		$tags = Request::post('langtag', array(), 'array');
		foreach ($tags as $tag => $value) {
			$tag = trim($tag);
			if (empty($tag)) {
				Message::addWarning('i18n-empty-tag');
				continue;
			}
			if (empty($value)) {
				unset($data[$tag]);
			} else {
				$data[$tag] = $value;
			}
		}
		$translation = Request::post('new-text', array(), 'array');
		foreach (Request::post('new-id', array(), 'array') as $k => $tag) {
			if (empty($translation[$k]) || empty($tag))
				continue;
			$data[(string)$tag] = (string)$translation[$k];
		}

		//saves the new values on the file
		$path = dirname($file);
		if (!is_dir($path)) {
			mkdir($path, 0775, true);
		}

		if (empty($data)) {
			if (file_exists($file)) {
				unlink($file);
			}
		} else {
			ksort($data); // Sort by key, so the diff on the output is cleaner
			$json = 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($file, $json) === false) {
				Message::addError('main.error-write', $file);
				return;
			}
		}

		Message::addSuccess('updated-tags');
	}

}