<?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);
}