eZ Publish  [4.0]
eztstranslator.php
Go to the documentation of this file.
00001 <?php
00002 //
00003 // Definition of eZTSTranslator class
00004 //
00005 // Created on: <07-Jun-2002 12:40:42 amos>
00006 //
00007 // ## BEGIN COPYRIGHT, LICENSE AND WARRANTY NOTICE ##
00008 // SOFTWARE NAME: eZ Publish
00009 // SOFTWARE RELEASE: 4.0.x
00010 // COPYRIGHT NOTICE: Copyright (C) 1999-2008 eZ Systems AS
00011 // SOFTWARE LICENSE: GNU General Public License v2.0
00012 // NOTICE: >
00013 //   This program is free software; you can redistribute it and/or
00014 //   modify it under the terms of version 2.0  of the GNU General
00015 //   Public License as published by the Free Software Foundation.
00016 //
00017 //   This program is distributed in the hope that it will be useful,
00018 //   but WITHOUT ANY WARRANTY; without even the implied warranty of
00019 //   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00020 //   GNU General Public License for more details.
00021 //
00022 //   You should have received a copy of version 2.0 of the GNU General
00023 //   Public License along with this program; if not, write to the Free
00024 //   Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
00025 //   MA 02110-1301, USA.
00026 //
00027 //
00028 // ## END COPYRIGHT, LICENSE AND WARRANTY NOTICE ##
00029 //
00030 
00031 /*! \file eztstranslator.php
00032 */
00033 
00034 /*!
00035   \class eZTSTranslator eztstranslator.php
00036   \ingroup eZTranslation
00037   \brief This provides internationalization using XML (.ts) files
00038 
00039 */
00040 
00041 //include_once( "lib/ezi18n/classes/eztranslatorhandler.php" );
00042 //include_once( "lib/ezi18n/classes/eztextcodec.php" );
00043 //include_once( "lib/ezi18n/classes/eztranslationcache.php" );
00044 
00045 class eZTSTranslator extends eZTranslatorHandler
00046 {
00047     /*!
00048      Construct the translator and loads the translation file $file if it is set and exists.
00049     */
00050     function eZTSTranslator( $locale, $filename = null, $useCache = true )
00051     {
00052         $this->UseCache = $useCache;
00053         if ( isset( $GLOBALS['eZSiteBasics'] ) )
00054         {
00055             $siteBasics = $GLOBALS['eZSiteBasics'];
00056             if ( isset( $siteBasics['no-cache-adviced'] ) && $siteBasics['no-cache-adviced'] )
00057                 $this->UseCache = false;
00058         }
00059         $this->BuildCache = false;
00060         $this->eZTranslatorHandler( true );
00061 
00062         $this->Locale = $locale;
00063         $this->File = $filename;
00064         $this->Messages = array();
00065         $this->CachedMessages = array();
00066         $this->HasRestoredCache = false;
00067         $this->RootCache = false;
00068     }
00069 
00070     /*!
00071      \static
00072      Initialize the ts translator and context if this is not already done.
00073     */
00074     static function initialize( $context, $locale, $filename, $useCache = true )
00075     {
00076         $instance = false;
00077         $file = $locale . '/' . $filename;
00078         if ( !empty( $GLOBALS['eZTSTranslationTables'][$file] ) )
00079         {
00080             $instance = $GLOBALS['eZTSTranslationTables'][$file];
00081             if ( $instance->hasInitializedContext( $context ) )
00082             {
00083                 return $instance;
00084             }
00085         }
00086 
00087         eZDebug::createAccumulatorGroup( 'tstranslator', 'TS translator' );
00088         eZDebug::accumulatorStart( 'tstranslator_init', 'tstranslator', 'TS init' );
00089         if ( !$instance )
00090         {
00091             $instance = new eZTSTranslator( $locale, $filename, $useCache );
00092             $GLOBALS['eZTSTranslationTables'][$file] = $instance;
00093             $manager = eZTranslatorManager::instance();
00094             $manager->registerHandler( $instance );
00095         }
00096         $instance->load( $context );
00097         eZDebug::accumulatorStop( 'tstranslator_init' );
00098         return $instance;
00099     }
00100 
00101     /*!
00102      \return true if the context \a $context is already initialized.
00103     */
00104     function hasInitializedContext( $context )
00105     {
00106         return isset( $this->CachedMessages[$context] );
00107     }
00108 
00109     /*!
00110      Tries to load the context \a $requestedContext for the translation and returns true if was successful.
00111     */
00112     function load( $requestedContext )
00113     {
00114         return $this->loadTranslationFile( $this->Locale, $this->File, $requestedContext );
00115     }
00116 
00117     /*!
00118      \private
00119     */
00120     function loadTranslationFile( $locale, $filename, $requestedContext )
00121     {
00122         //include_once( 'lib/ezfile/classes/ezdir.php' );
00123 
00124         // First try for current charset
00125         $charset = eZTextCodec::internalCharset();
00126         $tsTimeStamp = false;
00127 
00128         if ( !$this->RootCache )
00129         {
00130             $ini = eZINI::instance();
00131             $roots = array( $ini->variable( 'RegionalSettings', 'TranslationRepository' ) );
00132             $extensionBase = eZExtension::baseDirectory();
00133             $translationExtensions = $ini->variable( 'RegionalSettings', 'TranslationExtensions' );
00134             foreach ( $translationExtensions as $translationExtension )
00135             {
00136                 $extensionPath = $extensionBase . '/' . $translationExtension . '/translations';
00137                 if ( file_exists( $extensionPath ) )
00138                 {
00139                     $roots[] = $extensionPath;
00140                 }
00141             }
00142             $this->RootCache = array( 'roots' => $roots );
00143         }
00144         else
00145         {
00146             $roots = $this->RootCache['roots'];
00147             if ( isset( $this->RootCache['timestamp'] ) )
00148                 $tsTimeStamp = $this->RootCache['timestamp'];
00149         }
00150 
00151 
00152         // Load cached translations if possible
00153         if ( $this->UseCache == true )
00154         {
00155             if ( !$tsTimeStamp )
00156             {
00157                 foreach ( $roots as $root )
00158                 {
00159                     $path = eZDir::path( array( $root, $locale, $charset, $filename ) );
00160                     if ( file_exists( $path ) )
00161                     {
00162                         $timestamp = filemtime( $path );
00163                         if ( $timestamp > $tsTimeStamp )
00164                             $tsTimeStamp = $timestamp;
00165                     }
00166                     else
00167                     {
00168                         $path = eZDir::path( array( $root, $locale, $filename ) );
00169                         if ( file_exists( $path ) )
00170                         {
00171                             $timestamp = filemtime( $path );
00172                             if ( $timestamp > $tsTimeStamp )
00173                                 $tsTimeStamp = $timestamp;
00174                         }
00175                     }
00176                 }
00177                 $this->RootCache['timestamp'] = $tsTimeStamp;
00178             }
00179             $key = 'cachecontexts';
00180             if ( $this->HasRestoredCache or
00181                  eZTranslationCache::canRestoreCache( $key, $tsTimeStamp ) )
00182             {
00183                 eZDebug::accumulatorStart( 'tstranslator_cache_load', 'tstranslator', 'TS cache load' );
00184                 if ( !$this->HasRestoredCache )
00185                 {
00186                     if ( !eZTranslationCache::restoreCache( $key ) )
00187                     {
00188                         $this->BuildCache = true;
00189                     }
00190                     $contexts = eZTranslationCache::contextCache( $key );
00191                     if ( !is_array( $contexts ) )
00192                         $contexts = array();
00193                     $this->HasRestoredCache = $contexts;
00194                 }
00195                 else
00196                     $contexts = $this->HasRestoredCache;
00197                 if ( !$this->BuildCache )
00198                 {
00199                     $contextName = $requestedContext;
00200                     if ( !isset( $this->CachedMessages[$contextName] ) )
00201                     {
00202                         eZDebug::accumulatorStart( 'tstranslator_context_load', 'tstranslator', 'TS context load' );
00203                         if ( eZTranslationCache::canRestoreCache( $contextName, $tsTimeStamp ) )
00204                         {
00205                             if ( !eZTranslationCache::restoreCache( $contextName ) )
00206                             {
00207                                 $this->BuildCache = true;
00208                             }
00209                             $this->CachedMessages[$contextName] =
00210                                  eZTranslationCache::contextCache( $contextName );
00211 
00212                             foreach ( $this->CachedMessages[$contextName] as $key => $msg )
00213                             {
00214                                 $this->Messages[$key] = $msg;
00215                             }
00216                         }
00217                         eZDebug::accumulatorStop( 'tstranslator_context_load' );
00218                     }
00219                 }
00220                 eZDebugSetting::writeNotice( 'i18n-tstranslator', "Loading cached translation", "eZTSTranslator::loadTranslationFile" );
00221                 eZDebug::accumulatorStop( 'tstranslator_cache_load' );
00222                 if ( !$this->BuildCache )
00223                 {
00224                     return true;
00225                 }
00226             }
00227             eZDebugSetting::writeNotice( 'i18n-tstranslator',
00228                                          "Translation cache has expired. Will rebuild it from source.",
00229                                          "eZTSTranslator::loadTranslationFile" );
00230             $this->BuildCache = true;
00231         }
00232 
00233         $status = false;
00234         foreach ( $roots as $root )
00235         {
00236             $path = eZDir::path( array( $root, $locale, $charset, $filename ) );
00237             if ( !file_exists( $path ) )
00238             {
00239                 $path = eZDir::path( array( $root, $locale, $filename ) );
00240 
00241                 $ini = eZINI::instance( "i18n.ini" );
00242                 $fallbacks = $ini->variable( 'TranslationSettings', 'FallbackLanguages' );
00243 
00244                 if ( array_key_exists( $locale,  $fallbacks ) and $fallbacks[$locale] )
00245                 {
00246                     $fallbackpath = eZDir::path( array( $root, $fallbacks[$locale], $filename ) );
00247                     if ( !file_exists( $path ) and file_exists( $fallbackpath ) )
00248                         $path = $fallbackpath;
00249                 }
00250 
00251                 if ( !file_exists( $path ) )
00252                 {
00253                     eZDebug::writeError( "Could not load translation file: $path", "eZTSTranslator::loadTranslationFile" );
00254                     continue;
00255                 }
00256             }
00257 
00258             eZDebug::accumulatorStart( 'tstranslator_load', 'tstranslator', 'TS load' );
00259 
00260             $doc = new DOMDocument( '1.0', 'utf-8' );
00261             $success = $doc->load( $path );
00262 
00263             if ( !$success )
00264             {
00265                 eZDebug::writeWarning( "Unable to load XML from file $path", 'eZTSTranslator::loadTranslationFile' );
00266                 continue;
00267             }
00268 
00269             if ( !$this->validateDOMTree( $doc ) )
00270             {
00271                 eZDebug::writeWarning( "XML text for file $path did not validate", 'eZTSTranslator::loadTranslationFile' );
00272                 continue;
00273             }
00274 
00275             $status = true;
00276 
00277             $treeRoot = $doc->documentElement;
00278             $children = $treeRoot->childNodes;
00279             for ($i = 0; $i < $children->length; $i++ )
00280             {
00281                 $child = $children->item( $i );
00282 
00283                 if ( $child->nodeType == XML_ELEMENT_NODE )
00284                 {
00285                     if ( $child->tagName == "context" )
00286                     {
00287                         $this->handleContextNode( $child );
00288                     }
00289                 }
00290             }
00291             eZDebug::accumulatorStop( 'tstranslator_load' );
00292         }
00293 
00294         // Save translation cache
00295         if ( $this->UseCache == true && $this->BuildCache == true )
00296         {
00297             eZDebug::accumulatorStart( 'tstranslator_store_cache', 'tstranslator', 'TS store cache' );
00298             if ( eZTranslationCache::contextCache( 'cachecontexts' ) == null )
00299             {
00300                 $contexts = array_keys( $this->CachedMessages );
00301                 eZTranslationCache::setContextCache( 'cachecontexts',
00302                                                      $contexts );
00303                 eZTranslationCache::storeCache( 'cachecontexts' );
00304                 $this->HasRestoredCache = $contexts;
00305             }
00306 
00307             foreach ( $this->CachedMessages as $contextName => $context )
00308             {
00309                 if ( eZTranslationCache::contextCache( $contextName ) == null )
00310                     eZTranslationCache::setContextCache( $contextName, $context );
00311                 eZTranslationCache::storeCache( $contextName );
00312             }
00313             $this->BuildCache = false;
00314             eZDebug::accumulatorStop( 'tstranslator_store_cache' );
00315         }
00316 
00317         return $status;
00318     }
00319 
00320     /*!
00321      \static
00322      Validates the DOM tree \a $tree and returns true if it is correct.
00323     */
00324     static function validateDOMTree( $tree )
00325     {
00326         if ( !is_object( $tree ) )
00327             return false;
00328 
00329         $isValid = $tree->RelaxNGValidate( 'schemas/translation/ts.rng' );
00330 
00331         return $isValid;
00332     }
00333 
00334     function handleContextNode( $context )
00335     {
00336         $contextName = null;
00337         $messages = array();
00338         $context_children = $context->childNodes;
00339 
00340         for( $i = 0; $i < $context_children->length; $i++ )
00341         {
00342             $context_child = $context_children->item( $i );
00343             if ( $context_child->nodeType == XML_ELEMENT_NODE )
00344             {
00345                 if ( $context_child->tagName == "name" )
00346                 {
00347                     $name_el = $context_child->firstChild;
00348                     if ( $name_el )
00349                     {
00350                         $contextName = $name_el->nodeValue;
00351                     }
00352                 }
00353                 break;
00354             }
00355         }
00356         if ( !$contextName )
00357         {
00358             eZDebug::writeError( "No context name found, skipping context",
00359                                  "eZTSTranslator::handleContextNode" );
00360             return false;
00361         }
00362         foreach( $context_children as $context_child )
00363         {
00364             if ( $context_child->nodeType == XML_ELEMENT_NODE )
00365             {
00366                 $childName = $context_child->tagName;
00367                 if ( $childName == "message" )
00368                 {
00369                     $this->handleMessageNode( $contextName, $context_child );
00370                 }
00371                 else if ( $childName == "name" )
00372                 {
00373                     /* Skip name tags */
00374                 }
00375                 else
00376                 {
00377                     eZDebug::writeError( "Unknown element name: $childName",
00378                                          "eZTSTranslator::handleContextNode" );
00379                 }
00380             }
00381         }
00382         if ( $contextName === null )
00383         {
00384             eZDebug::writeError( "No context name found, skipping context",
00385                                  "eZTSTranslator::handleContextNode" );
00386             return false;
00387         }
00388         if ( !isset( $this->CachedMessages[$contextName] ) )
00389             $this->CachedMessages[$contextName] = array();
00390 
00391         return true;
00392     }
00393 
00394     function handleMessageNode( $contextName, $message )
00395     {
00396         $source = null;
00397         $translation = null;
00398         $comment = null;
00399         $message_children = $message->childNodes;
00400         for( $i = 0; $i < $message_children->length; $i++ )
00401         {
00402             $message_child = $message_children->item( $i );
00403             if ( $message_child->nodeType == XML_ELEMENT_NODE )
00404             {
00405                 $childName = $message_child->tagName;
00406                 if ( $childName  == "source" )
00407                 {
00408                     if ( $message_child->childNodes->length > 0 )
00409                     {
00410                         $source = '';
00411                         foreach ( $message_child->childNodes as $textEl )
00412                         {
00413                             if ( $textEl instanceof DOMText )
00414                             {
00415                                 $source .= $textEl->nodeValue;
00416                             }
00417                             else if ( $textEl instanceof DOMElement && $textEl->tagName == 'byte' )
00418                             {
00419                                 $source .= chr( intval( '0' . $textEl->getAttribute( 'value' ) ) );
00420                             }
00421                         }
00422                     }
00423                 }
00424                 else if ( $childName == "translation" )
00425                 {
00426                     if ( $message_child->childNodes->length > 0 )
00427                     {
00428                         $translation = '';
00429                         foreach ( $message_child->childNodes as $textEl )
00430                         {
00431                             if ( $textEl instanceof DOMText )
00432                             {
00433                                 $translation .= $textEl->nodeValue;
00434                             }
00435                             else if ( $textEl instanceof DOMElement && $textEl->tagName == 'byte' )
00436                             {
00437                                 $translation .= chr( intval( '0' . $textEl->getAttribute( 'value' ) ) );
00438                             }
00439                         }
00440                     }
00441                 }
00442                 else if ( $childName == "comment" )
00443                 {
00444                     $comment_el = $message_child->firstChild;
00445                     $comment = $comment_el->nodeValue;
00446                 }
00447                 else if ( $childName == "translatorcomment" )
00448                 {
00449                     //Ignore it.
00450                 }
00451                 else if ( $childName == "location" )
00452                 {
00453                     //Handle location element. No functionality yet.
00454                 }
00455                 else
00456                     eZDebug::writeError( "Unknown element name: " . $childName,
00457                                          "eZTSTranslator::handleMessageNode" );
00458             }
00459         }
00460         if ( $source === null )
00461         {
00462             eZDebug::writeError( "No source name found, skipping message",
00463                                  "eZTSTranslator::handleMessageNode" );
00464             return false;
00465         }
00466         if ( $translation === null )
00467         {
00468 //             eZDebug::writeError( "No translation, skipping message", "eZTSTranslator::messageNode" );
00469             return false;
00470         }
00471         /* we need to convert ourselves if we're using libxml stuff here */
00472         if ( $message instanceof DOMElement )
00473         {
00474             $codec = eZTextCodec::instance( "utf8" );
00475             $source = $codec->convertString( $source );
00476             $translation = $codec->convertString( $translation );
00477             $comment = $codec->convertString( $comment );
00478         }
00479 
00480         $this->insert( $contextName, $source, $translation, $comment );
00481         return true;
00482     }
00483 
00484     /*!
00485      \reimp
00486     */
00487     function findKey( $key )
00488     {
00489         $msg = null;
00490         if ( isset( $this->Messages[$key] ) )
00491         {
00492             $msg = $this->Messages[$key];
00493         }
00494         return $msg;
00495     }
00496 
00497     /*!
00498      \reimp
00499     */
00500     function findMessage( $context, $source, $comment = null )
00501     {
00502         // First try with comment,
00503         $man = eZTranslatorManager::instance();
00504         $key = $man->createKey( $context, $source, $comment );
00505 
00506         if ( !isset( $this->Messages[$key] ) )
00507         {
00508             // then try without comment for general translation
00509             $key = $man->createKey( $context, $source );
00510         }
00511 
00512         return $this->findKey( $key );
00513     }
00514 
00515     /*!
00516      \reimp
00517     */
00518     function keyTranslate( $key )
00519     {
00520         $msg = $this->findKey( $key );
00521         if ( $msg !== null )
00522             return $msg["translation"];
00523         else
00524         {
00525             return null;
00526         }
00527     }
00528 
00529     /*!
00530      \reimp
00531     */
00532     function translate( $context, $source, $comment = null )
00533     {
00534         $msg = $this->findMessage( $context, $source, $comment );
00535         if ( $msg !== null )
00536         {
00537             return $msg["translation"];
00538         }
00539 
00540         return null;
00541     }
00542 
00543     /*!
00544      Inserts the \a $translation for the \a $context and \a $source as a translation message
00545      and returns the key for the message. If $comment is non-null it will be included in the message.
00546 
00547      If the translation message exists no new message is created and the existing key is returned.
00548     */
00549     function insert( $context, $source, $translation, $comment = null )
00550     {
00551 //         eZDebug::writeDebug( "context=$context" );
00552         if ( $context == "" )
00553             $context = "default";
00554         $man = eZTranslatorManager::instance();
00555         $key = $man->createKey( $context, $source, $comment );
00556 //         if ( isset( $this->Messages[$key] ) )
00557 //             return $key;
00558         $msg = $man->createMessage( $context, $source, $comment, $translation );
00559         $msg["key"] = $key;
00560         $this->Messages[$key] = $msg;
00561         // Set array of messages to be cached
00562         if ( $this->UseCache == true && $this->BuildCache == true )
00563         {
00564             if ( !isset( $this->CachedMessages[$context] ) )
00565                 $this->CachedMessages[$context] = array();
00566             $this->CachedMessages[$context][$key] = $msg;
00567         }
00568         return $key;
00569     }
00570 
00571     /*!
00572      Removes the translation message with \a $context and \a $source.
00573      Returns true if the message was removed, false otherwise.
00574 
00575      If you have the translation key use removeKey() instead.
00576     */
00577     function remove( $context, $source, $message = null )
00578     {
00579         if ( $context == "" )
00580             $context = "default";
00581         $man = eZTranslatorManager::instance();
00582         $key = $man->createKey( $context, $source, $message );
00583         if ( isset( $this->Messages[$key] ) )
00584             unset( $this->Messages[$key] );
00585     }
00586 
00587     /*!
00588      Removes the translation message with \a $key.
00589      Returns true if the message was removed, false otherwise.
00590     */
00591     function removeKey( $key )
00592     {
00593         if ( isset( $this->Messages[$key] ) )
00594             unset( $this->Messages[$key] );
00595     }
00596 
00597     /*!
00598      \static
00599      Fetche list of available translations, create eZTrnslator for each translations.
00600      \return list of eZTranslator objects representing available translations.
00601     */
00602     static function fetchList( $localeList = array() )
00603     {
00604         //include_once( 'lib/ezutils/classes/ezini.php' );
00605         $ini = eZINI::instance();
00606 
00607         $dir = $ini->variable( 'RegionalSettings', 'TranslationRepository' );
00608 
00609         $fileInfoList = array();
00610         $translationList = array();
00611         $locale = '';
00612 
00613         //include_once( 'lib/ezfile/classes/ezfile.php' );
00614 
00615         if ( count( $localeList ) == 0 )
00616         {
00617             $localeList = eZDir::findSubdirs( $dir );
00618         }
00619 
00620         foreach( $localeList as $locale )
00621         {
00622             if ( $locale != 'untranslated' )
00623             {
00624                 $translationFiles = eZDir::findSubitems( $dir . '/' . $locale, 'f' );
00625 
00626                 foreach( $translationFiles as $translationFile )
00627                 {
00628                     if ( eZFile::suffix( $translationFile ) == 'ts' )
00629                     {
00630                         $translationList[] = new eZTSTranslator( $locale,  $translationFile );
00631                     }
00632                 }
00633             }
00634         }
00635 
00636         return $translationList;
00637     }
00638 
00639     /*!
00640      \static
00641     */
00642     static function resetGlobals()
00643     {
00644         unset( $GLOBALS["eZTSTranslationTables"] );
00645     }
00646 
00647     /// \privatesection
00648     /// Contains the hash table with message translations
00649     public $Messages;
00650     public $File;
00651     public $UseCache;
00652     public $BuildCache;
00653     public $CachedMessages;
00654 }
00655 
00656 ?>