Location: PHPKode > scripts > icI18n > icI18n.class.php
<?php
/**
 * Helper classes which can be used to create Multi-lingual applications in php
 *
 * @category   I18n
 * @author     Igor Crevar <hide@address.com>
 * @license    http://www.opensource.org/licenses/mit-license.php
 * @version    0.8
 * @link       http://pear.php.net/package/PackageName
 */

/**
 * Simple extending of base exception class
 */  
class icI18nException extends Exception{
}
 
/**
 * icI18Language represents one language with all translations, etc
 */ 
class icI18nLanguage
{
	/**
     * String representation of languageName(or culture)
     *
     * Values normaly will be something en, en_US,...
     *
     * @var string
     */
	protected $languageName;
	/**
     * All translations are kept in this attribute
     *
     * Keys in array is names of namespaces. Values are arrays where keys are original strings and values are translated strings
     *
     * @var array of arrays
     */
	protected $translations;
	
	/**
     * Its static string attribute which holds base path of directory where all language files are. Cached(php prepared) translations are in basePath+'/cache'
     *
     * You must init basePath before first instance of icI18nLanguage
     *
     * @var string $bp
     */
	protected static $basePath = NULL;
	
	/*
	 * use to init shared base path  for all languages files
	 */
	public function setBasePath($bp)
	{
		//add / if last char in $bp is not / or \ 
		if ( strlen($bp) &&  !in_array($bp[strlen($bp)-1], array('/','\\')) )
		{
			$bp .= DIRECTORY_SEPARATOR;
		}
		self::$basePath = $bp;
	}

	/*
	 * Constrcutor for icI18n. 
	 * @param string $languageName name of language 
	 * @throws icI18nException if $basePath is not set
	 */
	public function __construct($languageName)
	{
		$this->languageName = $languageName;
		$this->translations = array();
		//if static variable base path is not initialized init it now
		if ( !self::$basePath )
		{
			throw new icI18nException('Base path for all languages must be defined. Use static function setBasePath');
		}
		$this->load();
	}
	
	/*
	 * returns name of language
	 */
	public function getLanguageName()
	{
		return $this->languageName;
	}
	
	/*
	 * clears all translations
	 */
	public function clearTranslations()
	{
		$this->translations = array();
	}
	
	/*
	 * gets copy of translations array
	 */
	public function getTranslations()
	{
		//be sure to return new instance
		return array_merge($this->translations, array() );
	}
	
	/*
	 * @return true if there is at least one namespace in object
	 */
	public function isEmpty()
	{
		return count($this->translations) == 0;
	}
	
	/*
	 * Erases cached php file
	 * 
	 */
	public function clearCache()
	{
		@unlink( self::$basePath.'cache/'.$this->languageName.'.php' );
	}
	
	
	/*
	 * returns translation of desired string($key) within desired $namespace
	 * @param string $key - key to translate
	 * @param string $namespace - namespace
	 * @return string translated key within namespace if exist otherwise key
	 */
	public function __($key, $namespace)
	{
		if ( isset($this->translations[$namespace][$key]) )
		{
			return $this->translations[$namespace][$key];
		} 
		return $key;
	}
	
	/*
	 * Loads translations from cached file or call loadTranslation for retrieving translations from file
	 * 
	 * file path is $basePath + 'cache/'+languageName+'.php'
	 */
	
	public function load()
	{	
		//if cached file exists load it from there
		$cacheFile = self::$basePath.'cache/'.$this->languageName.'.php';
		
		//create cache dir if not exist
		if ( !is_dir(self::$basePath.'cache') )
		{
			mkdir(self::$basePath.'cache');
		}
		if ( file_exists($cacheFile) )
		{
			include $cacheFile;
			$this->translations = $t;
			return;
		}	
		//load translation from xml or xls file
		$this->loadFromFile();
		file_put_contents($cacheFile, $this->toPHPStr(), LOCK_EX );
	}
	
	/*
	 * Loads translation from xml file
	 * 
	 * Format of xml is
	 * <?xml version="1.0" encoding="utf-8"?>
     * <translations>
 	 * <namespace name="NAMESPACE_NAME">
     *   <one name="ORIGINAL_STRING_NAME">TRANSLATED_STRING_NAME</one>
     *   ....
  	 *  </namespace>
  	 *  ....
  	 *  </translations>
	 *
	 * In subclass(es) you can override this method if you want for example to parse some other file type like xls
	 */
	protected function loadFromFile()
	{
		//parse xml and retrieve translates
		$xmlFilePath = self::$basePath.$this->languageName.'.xml';
		if ( !file_exists($xmlFilePath) ) return;
		$xmlContent = file_get_contents($xmlFilePath);
		$oXML = new DOMDocument();
		$oXML->loadXML( $xmlContent ); 
		$namespaceNodeList = $oXML->getElementsByTagName('namespace');
		
		for ($i = 0; $i < $namespaceNodeList->length; ++$i)
		{
			$namespaceEl = $namespaceNodeList->item($i);
			if($namespaceEl->childNodes->length) {
            	$namespace = $namespaceEl->attributes->getNamedItem("name")->nodeValue;;
            	if ( !isset($this->translations[$namespace]) )
            	{
            		$this->translations[$namespace] = array();
            	}
            	
            	for ($j = 0; $j < $namespaceEl->childNodes->length; ++$j)
            	{
            		$translateEl = $namespaceEl->childNodes->item($j);
            		//skip no "one" nodes
            		if ('one' != $translateEl->nodeName) continue;
            		
		 			$key = $translateEl->attributes->getNamedItem("name")->nodeValue;;
                	$this->translations[$namespace][$key] = trim($translateEl->nodeValue);
            	}
       		}
		}
		
		
	} 

	/*
	 * add new translation to table of translations
	 * 
	 * @param string $key - key 
	 * @param string $value - value to add
	 * @param string $namespace - default is 'default
	 * @return reference to icI18nLanguage object
	 */
	public function addTranslation($key, $value, $namespace = 'default')
	{
		if ( !isset($this->translations[$namespace]) )
       	{
           	$this->translations[$namespace] = array();
       	}
		$this->translations[$namespace][$key] = $value;
		return $this;
	}
	
	/*
	 * convert translation array to PHP code string
	 * 
	 * for example you can use this method to store PHP code of translations into DB or some other storage 
	 *
	 * @param boolean $withPrefix - should we add <?php in front of res string
	 * @return string PHP code of translation table
	 */
	public function toPHPStr($withPrefix = true)
	{		
		$cacheStr = '';
		foreach ($this->translations as $namespace => $translatePairs )
		{
			$tmpCacheStr = '';
			foreach ($translatePairs as $key => $value)
			{
			 	if ($tmpCacheStr) $tmpCacheStr .= ',';
             	$tmpCacheStr .= $this->phpString($key).' => '.$this->phpString($value);
			}
			if ($cacheStr) $cacheStr .= ',';
            $cacheStr .= $this->phpString($namespace).' => array('.$tmpCacheStr.')';
		}
		
		$cacheStr = '$t = array('.$cacheStr.');';
		if ($withPrefix)
		{
			$cacheStr = "<?php $cacheStr";
		}
		return $cacheStr;
	}
	
	/*
	 * reverse - put all tranlations to xml file.
	 * 
	 * Use this together with findAllTranslationStrings
	 *
	 */
	public function toFile()
	{		
		$oXML = new DOMDocument('1.0', 'utf-8');
		//we want a nice output
		$oXML->formatOutput = true;
		$rootEl = $oXML->createElement('translations');
		$rootEl = $oXML->appendChild($rootEl);
		
		foreach ($this->translations as $namespace => $translatePairs )
		{
			$namespaceEl = $oXML->createElement('namespace');
			$namespaceEl->setAttribute('name', $namespace);
			$namespaceEl = $rootEl->appendChild($namespaceEl);
			foreach ($translatePairs as $key => $value)
			{
			 	$oneEl = $oXML->createElement('one', $value);
				$oneEl->setAttribute('name', $key);
				$namespaceEl->appendChild($oneEl);
			}
			
		}
		//finally save generated xml to file
		file_put_contents( self::$basePath.$this->languageName.'.xml', $oXML->saveXML(), LOCK_EX );
		//clear cached
		$this->clearCache();
	}
	
	/*
	 * Search directory $dir for all __('') or __("") __('','') or __("","") inside *.php,*.inc files and put strings in translation array
	 * 
	 * @param string $dir - root directory from where to start searching
	 * @param boolean $recursive - should we recursive search directories?
	 */
	public function findAllTranslationStrings($dir, $recursive = true)
	{
		//$content = '__(\'Yurgen\') je bog a ja sam __("SLIG",  "DD")';
		//make complicate preg match pattern
		$namespacePattern = '(?<q1>"|\')(?P<namespace>.+?)(?P=q1)';
		$namespaceOptionalPattern = "(,[ ]*$namespacePattern)?";
		$keyPattern = '(?<q>"|\')(?P<key>.+?)(?P=q)'.$namespaceOptionalPattern;
		$pattern = "#__\($keyPattern\)#";
										
		//be sure to have /(or \ for windows) in the end of string
		if ( $dir[strlen($dir)-1] != DIRECTORY_SEPARATOR )
		{
			$dir .= DIRECTORY_SEPARATOR;
		}
		//try to open handle
		if ( !($handle = opendir($dir)) ) return; 
		while (false !== ($file = readdir($handle))) 
		{
			//skip . and ..
			if ( $file == '..' ||  $file == '.' ) continue; 	
			$fullFilePath = $dir.$file;
			//if its dir
			if ( is_dir($fullFilePath) )
			{
				if ($recursive)
				{
					$this->findAllTranslationStrings($dir.$file);
				}
				continue;
			}
			$len = strlen($file);
			//its not this file and its extension ends with .php or .inc
			if ( $len > 5 && $file != __FILE__ &&  in_array( substr($file, $len - 4, 4), array('.php', '.inc') ) )
			{
				$content = file_get_contents($fullFilePath);
				//complicated preg_match :)
				preg_match_all($pattern, $content, $aMatches);
				if ( isset($aMatches['key']) )
				{
					foreach ( $aMatches['key'] as $ind => $key )
					{
						$namespace = !empty($aMatches['namespace'][$ind]) ? $aMatches['namespace'][$ind] : 'default';
						$this->translations[$namespace][$key] = ''; 
					}
				}
			}
		}
		
		closedir($handle);
		
	}
	
	//changes " in $s to \" and returns "$s"
	private function phpString($s)
	{
		return '"'.str_replace( array('"'), array('\"'), $s).'"';
	}
		
}

/*
 * Base class. Provides static methods for manipulation with languages
 */
class icI18n
{	
	/*
	 * @var string
	 * Default language name
	 * Default languaga should not have icI18nLanguage instance!!!
	 * Default language is not translated!!!
	 */
	protected static $defaultLanguageName = '__undefined';
	
	/*
	 * @var string
	 * Holds name of current language
	 */
	protected static $currentLanguageName = '__undefined';
	/*
	 * keeps all languages. keys are language names 
	 * @var array of icI18nLanguage
	 */
	protected static $instances = array();
	
	/*
	 * Initalization 
	 *
	 * You can manualy calls icI18n::setCurrentLanguage and icI18n:setBasePath but its better to call this before you do anything with these classes
	 *
	 * @param string $basePath - use for proxy calling icI18n::setBasePath($basePath);
	 * @param string $currentLanguageName - current language name
	 */
	public static function init($defaultLanguageName, $basePath, $currentLanguageName = '__undefined')
	{
		//set default language name
		self::setDefaultLanguage($defaultLanguageName);
		//set current language name
		self::setCurrentLanguage($currentLanguageName);
		//set base directory where all language files are
		icI18nLanguage::setBasePath($basePath);
	}
	
	/*
	 * Proxy for __ method of current language object
	 * @param string $key - key to translate
	 * @param string $namespace - namespace
	 * @return string translated key within namespace if exist otherwise key
	 */
	public static function __($key, $namespace = 'default')
	{
		//default language is not translated
		if ( self::$defaultLanguageName == self::$currentLanguageName ) return $key;
		return self::getLanguage()->__($key, $namespace);
	}
		
	/*
	 * Add new language to array of available languages
	 * @param icI18nLanguage $obj instance of icI18nLanguage object
	 * @throws icI18nException
	 */
	public static function addLanguage(/* icI18nLanguage */ $obj)
	{
		if ( $obj->getLanguageName() == self::$defaultLanguageName )
		{
			throw new icI18nException("Default language instance can not be added!");
		}
		self::$instances[$obj->getLanguageName()] = $obj;
	}
	
	/*
	 * Set current language name
	 * @param icI18nLanguage or string $language
	 */
	public static function setCurrentLanguage($language)
	{
		if ( is_object($language) )
		{
			self::$currentLanguageName = $language->getLanguageName();
			return;
		}
		self::$currentLanguageName = $language;
	}
	
	/*
	 * Set default language name
	 * this language will not be translated!!!
	 * @param string $language name of language
	 */
	public static function setDefaultLanguage($language)
	{
		self::$defaultLanguageName = $language;
	}
	
	/*
	 * get instance of icI18nLanguage
	 *
	 * @param string $name you can provide name of language you want - if not specified class returns instance of current language name
	 * @return instance of icI18nLanguage
	 * @throws icI18nException if language with desired name is not yet created
	 */
	public static function getLanguage($name = false)
	{
		//pick from current language name if not specified
		if (!$name) $name = self::$currentLanguageName;
		//if already created return from assoc array
		if ( isset(self::$instances[$name]) )
		{
			return self::$instances[$name];
		}
		//language does not exist throw Exeption
		throw new icI18nException("$name does not exist! Use static method addLanguage");		
	}
}


/*
 * Helper - see icI18n::__
 */
function __($key, $namespace = 'default')
{
	return icI18n::__($key, $namespace);
}
Return current item: icI18n