Exponential 6.0.13 Exponential Enterprise Open Source CMS - PHP Class Reference

eztstranslator.php
Go to the documentation of this file.
1 <?php
16 class eZTSTranslator extends eZTranslatorHandler
17 {
24 public function __construct( $locale, $filename = null, $useCache = true )
25 {
26 $this->UseCache = $useCache;
27 if ( isset( $GLOBALS['eZSiteBasics'] ) )
28 {
29 $siteBasics = $GLOBALS['eZSiteBasics'];
30 if ( isset( $siteBasics['no-cache-adviced'] ) && $siteBasics['no-cache-adviced'] )
31 $this->UseCache = false;
32 }
33 $this->BuildCache = false;
34 parent::__construct( true );
35
36 $this->Locale = $locale;
37 $this->File = $filename;
38 $this->Messages = array();
39 $this->CachedMessages = array();
40 $this->HasRestoredCache = false;
41 $this->RootCache = false;
42 }
43
53 static function initialize( $context, $locale, $filename, $useCache = true )
54 {
55 $instance = false;
56 $file = $locale . '/' . $filename;
57 if ( !empty( $GLOBALS['eZTSTranslationTables'][$file] ) )
58 {
59 $instance = $GLOBALS['eZTSTranslationTables'][$file];
60 if ( $instance->hasInitializedContext( $context ) )
61 {
62 return $instance;
63 }
64 }
65
66 eZDebug::createAccumulatorGroup( 'tstranslator', 'TS translator' );
67 eZDebug::accumulatorStart( 'tstranslator_init', 'tstranslator', 'TS init' );
68 if ( !$instance )
69 {
70 $instance = new eZTSTranslator( $locale, $filename, $useCache );
71 $GLOBALS['eZTSTranslationTables'][$file] = $instance;
72 $manager = eZTranslatorManager::instance();
73 $manager->registerHandler( $instance );
74 }
75 $instance->load( $context );
76 eZDebug::accumulatorStop( 'tstranslator_init' );
77 return $instance;
78 }
79
86 function hasInitializedContext( $context )
87 {
88 return isset( $this->CachedMessages[$context] );
89 }
90
97 function load( $requestedContext )
98 {
99 return $this->loadTranslationFile( $this->Locale, $this->File, $requestedContext );
100 }
101
115 function loadTranslationFile( $locale, $filename, $requestedContext )
116 {
117 // First try for current charset
118 $charset = eZTextCodec::internalCharset();
119 $tsTimeStamp = false;
120 $ini = eZINI::instance();
121 $checkMTime = $ini->variable( 'RegionalSettings', 'TranslationCheckMTime' ) === 'enabled';
122
123 if ( !$this->RootCache )
124 {
125 $roots = array( $ini->variable( 'RegionalSettings', 'TranslationRepository' ) );
126 $extensionBase = eZExtension::baseDirectory();
127 $translationExtensions = $ini->variable( 'RegionalSettings', 'TranslationExtensions' );
128 foreach ( $translationExtensions as $translationExtension )
129 {
130 $extensionPath = $extensionBase . '/' . $translationExtension . '/translations';
131 if ( !$checkMTime || file_exists( $extensionPath ) )
132 {
133 $roots[] = $extensionPath;
134 }
135 }
136 $this->RootCache = array( 'roots' => $roots );
137 }
138 else
139 {
140 $roots = $this->RootCache['roots'];
141 if ( isset( $this->RootCache['timestamp'] ) )
142 $tsTimeStamp = $this->RootCache['timestamp'];
143 }
144
145
146 // Load cached translations if possible
147 if ( $this->UseCache == true )
148 {
149 if ( !$tsTimeStamp )
150 {
151 $expiry = eZExpiryHandler::instance();
152 $globalTsTimeStamp = $expiry->getTimestamp( self::EXPIRY_KEY, 0 );
153 $localeTsTimeStamp = $expiry->getTimestamp( self::EXPIRY_KEY . '-' . $locale, 0 );
154 $tsTimeStamp = max( $globalTsTimeStamp, $localeTsTimeStamp );
155 if ( $checkMTime && $tsTimeStamp < time() )// no need if ts == time()
156 {
157 // iterate over each known TS file, and get the highest timestamp
158 // this value will be used to check for cache validity
159 foreach ( $roots as $root )
160 {
161 $path = eZDir::path( array( $root, $locale, $charset, $filename ) );
162 if ( file_exists( $path ) )
163 {
164 $timestamp = filemtime( $path );
165 if ( $timestamp > $tsTimeStamp )
166 $tsTimeStamp = $timestamp;
167 }
168 else
169 {
170 $path = eZDir::path( array( $root, $locale, $filename ) );
171 if ( file_exists( $path ) )
172 {
173 $timestamp = filemtime( $path );
174 if ( $timestamp > $tsTimeStamp )
175 $tsTimeStamp = $timestamp;
176 }
177 }
178 }
179 }
180 $this->RootCache['timestamp'] = $tsTimeStamp;
181 }
182
183 $key = 'cachecontexts';
184 if ( $this->HasRestoredCache or
185 eZTranslationCache::canRestoreCache( $key, $tsTimeStamp ) )
186 {
187 eZDebug::accumulatorStart( 'tstranslator_cache_load', 'tstranslator', 'TS cache load' );
188 if ( !$this->HasRestoredCache )
189 {
190 if ( !eZTranslationCache::restoreCache( $key ) )
191 {
192 $this->BuildCache = true;
193 }
194 $contexts = eZTranslationCache::contextCache( $key );
195 if ( !is_array( $contexts ) )
196 $contexts = array();
197 $this->HasRestoredCache = $contexts;
198 }
199 else
200 $contexts = $this->HasRestoredCache;
201 if ( !$this->BuildCache )
202 {
203 $contextName = $requestedContext;
204 if ( !isset( $this->CachedMessages[$contextName] ) )
205 {
206 eZDebug::accumulatorStart( 'tstranslator_context_load', 'tstranslator', 'TS context load' );
207 if ( eZTranslationCache::canRestoreCache( $contextName, $tsTimeStamp ) )
208 {
209 if ( !eZTranslationCache::restoreCache( $contextName ) )
210 {
211 $this->BuildCache = true;
212 }
213 $this->CachedMessages[$contextName] =
214 eZTranslationCache::contextCache( $contextName );
215
216 foreach ( $this->CachedMessages[$contextName] as $key => $msg )
217 {
218 $this->Messages[$key] = $msg;
219 }
220 }
221 eZDebug::accumulatorStop( 'tstranslator_context_load' );
222 }
223 }
224 eZDebugSetting::writeNotice( 'i18n-tstranslator', "Loading cached translation", __METHOD__ );
225 eZDebug::accumulatorStop( 'tstranslator_cache_load' );
226 if ( !$this->BuildCache )
227 {
228 return true;
229 }
230 }
231 eZDebugSetting::writeNotice( 'i18n-tstranslator',
232 "Translation cache has expired. Will rebuild it from source.",
233 __METHOD__ );
234 $this->BuildCache = true;
235 }
236
237 $status = false;
238
239 // first process country translation files
240 // then process country variation translation files
241 $localeParts = explode( '@', $locale );
242
243 $triedPaths = array();
244 $loadedPaths = array();
245
246 $ini = eZINI::instance( "i18n.ini" );
247 $fallbacks = $ini->variable( 'TranslationSettings', 'FallbackLanguages' );
248
249 foreach ( $localeParts as $localePart )
250 {
251 $localeCodeToProcess = isset( $localeCodeToProcess ) ? $localeCodeToProcess . '@' . $localePart: $localePart;
252
253 // array with alternative subdirs to check
254 $alternatives = array(
255 array( $localeCodeToProcess, $charset, $filename ),
256 array( $localeCodeToProcess, $filename ),
257 );
258
259 if ( isset( $fallbacks[$localeCodeToProcess] ) && $fallbacks[$localeCodeToProcess] )
260 {
261 if ( $fallbacks[$localeCodeToProcess] === 'eng-GB' ) // Consider eng-GB fallback as "untranslated" since eng-GB does not provide any ts file
262 {
263 $fallbacks[$localeCodeToProcess] = 'untranslated';
264 }
265 $alternatives[] = array( $fallbacks[$localeCodeToProcess], $charset, $filename );
266 $alternatives[] = array( $fallbacks[$localeCodeToProcess], $filename );
267 }
268
269 foreach ( $roots as $root )
270 {
271 if ( !file_exists( $root ) )
272 {
273 continue;
274 }
275
276 unset( $path );
277
278 foreach ( $alternatives as $alternative )
279 {
280 $pathParts = $alternative;
281 array_unshift( $pathParts, $root );
282 $pathToTry = eZDir::path( $pathParts );
283 $triedPaths[] = $pathToTry;
284
285 if ( file_exists( $pathToTry ) )
286 {
287 $path = $pathToTry;
288 break;
289 }
290 }
291
292 if ( !isset( $path ) )
293 {
294 continue;
295 }
296
297 eZDebug::accumulatorStart( 'tstranslator_load', 'tstranslator', 'TS load' );
298
299 $doc = new DOMDocument( '1.0', 'utf-8' );
300 $success = $doc->load( $path );
301
302 if ( !$success )
303 {
304 eZDebug::writeWarning( "Unable to load XML from file $path", __METHOD__ );
305 continue;
306 }
307
308 if ( !$this->validateDOMTree( $doc ) )
309 {
310 eZDebug::writeWarning( "XML text for file $path did not validate", __METHOD__ );
311 continue;
312 }
313
314 $loadedPaths[] = $path;
315
316 $status = true;
317
318 $treeRoot = $doc->documentElement;
319 $children = $treeRoot->childNodes;
320 for ($i = 0; $i < $children->length; $i++ )
321 {
322 $child = $children->item( $i );
323
324 if ( $child->nodeType == XML_ELEMENT_NODE )
325 {
326 if ( $child->tagName == "context" )
327 {
328 $this->handleContextNode( $child );
329 }
330 }
331 }
332 eZDebug::accumulatorStop( 'tstranslator_load' );
333 }
334 }
335
336 eZDebugSetting::writeDebug( 'i18n-tstranslator', implode( PHP_EOL, $triedPaths ), __METHOD__ . ': tried paths' );
337 eZDebugSetting::writeDebug( 'i18n-tstranslator', implode( PHP_EOL, $loadedPaths ), __METHOD__ . ': loaded paths' );
338
339 // Save translation cache
340 if ( $this->UseCache == true && $this->BuildCache == true )
341 {
342 eZDebug::accumulatorStart( 'tstranslator_store_cache', 'tstranslator', 'TS store cache' );
343 if ( eZTranslationCache::contextCache( 'cachecontexts' ) == null )
344 {
345 $contexts = array_keys( $this->CachedMessages );
346 eZTranslationCache::setContextCache( 'cachecontexts',
347 $contexts );
348 eZTranslationCache::storeCache( 'cachecontexts' );
349 $this->HasRestoredCache = $contexts;
350 }
351
352 foreach ( $this->CachedMessages as $contextName => $context )
353 {
354 if ( eZTranslationCache::contextCache( $contextName ) == null )
355 eZTranslationCache::setContextCache( $contextName, $context );
356 eZTranslationCache::storeCache( $contextName );
357 }
358
359 $this->BuildCache = false;
360 eZDebug::accumulatorStop( 'tstranslator_store_cache' );
361 }
362
363 return $status;
364 }
365
371 static function validateDOMTree( $tree )
372 {
373 if ( !is_object( $tree ) )
374 return false;
375
376 $isValid = $tree->RelaxNGValidate( 'schemas/translation/ts.rng' );
377
378 return $isValid;
379 }
380
386 function handleContextNode( $context )
387 {
388 $contextName = null;
389 $messages = array();
390 $context_children = $context->childNodes;
391
392 for( $i = 0; $i < $context_children->length; $i++ )
393 {
394 $context_child = $context_children->item( $i );
395 if ( $context_child->nodeType == XML_ELEMENT_NODE )
396 {
397 if ( $context_child->tagName == "name" )
398 {
399 $name_el = $context_child->firstChild;
400 if ( $name_el )
401 {
402 $contextName = $name_el->nodeValue;
403 }
404 }
405 break;
406 }
407 }
408 if ( !$contextName )
409 {
410 eZDebug::writeError( "No context name found, skipping context", __METHOD__ );
411 return false;
412 }
413 foreach( $context_children as $context_child )
414 {
415 if ( $context_child->nodeType == XML_ELEMENT_NODE )
416 {
417 $childName = $context_child->tagName;
418 if ( $childName == "message" )
419 {
420 $this->handleMessageNode( $contextName, $context_child );
421 }
422 else if ( $childName == "name" )
423 {
424 /* Skip name tags */
425 }
426 else
427 {
428 eZDebug::writeError( "Unknown element name: $childName", __METHOD__ );
429 }
430 }
431 }
432 if ( $contextName === null )
433 {
434 eZDebug::writeError( "No context name found, skipping context", __METHOD__ );
435 return false;
436 }
437 if ( !isset( $this->CachedMessages[$contextName] ) )
438 $this->CachedMessages[$contextName] = array();
439
440 return true;
441 }
442
448 function handleMessageNode( $contextName, $message )
449 {
450 $source = null;
451 $translation = null;
452 $comment = null;
453 $message_children = $message->childNodes;
454 for( $i = 0; $i < $message_children->length; $i++ )
455 {
456 $message_child = $message_children->item( $i );
457 if ( $message_child->nodeType == XML_ELEMENT_NODE )
458 {
459 $childName = $message_child->tagName;
460 if ( $childName == "source" )
461 {
462 if ( $message_child->childNodes->length > 0 )
463 {
464 $source = '';
465 foreach ( $message_child->childNodes as $textEl )
466 {
467 if ( $textEl instanceof DOMText )
468 {
469 $source .= $textEl->nodeValue;
470 }
471 else if ( $textEl instanceof DOMElement && $textEl->tagName == 'byte' )
472 {
473 $source .= chr( intval( '0' . $textEl->getAttribute( 'value' ) ) );
474 }
475 }
476 }
477 }
478 else if ( $childName == "translation" )
479 {
480 if ( $message_child->childNodes->length > 0 )
481 {
482 $translation = '';
483 foreach ( $message_child->childNodes as $textEl )
484 {
485 if ( $textEl instanceof DOMText )
486 {
487 $translation .= $textEl->nodeValue;
488 }
489 else if ( $textEl instanceof DOMElement && $textEl->tagName == 'byte' )
490 {
491 $translation .= chr( intval( '0' . $textEl->getAttribute( 'value' ) ) );
492 }
493 }
494 }
495 }
496 else if ( $childName == "comment" )
497 {
498 $comment_el = $message_child->firstChild;
499 $comment = $comment_el->nodeValue;
500 }
501 else if ( $childName == "translatorcomment" )
502 {
503 //Ignore it.
504 }
505 else if ( $childName == "location" )
506 {
507 //Handle location element. No functionality yet.
508 }
509 else
510 eZDebug::writeError( "Unknown element name: " . $childName, __METHOD__ );
511 }
512 }
513 if ( $source === null )
514 {
515 eZDebug::writeError( "No source name found, skipping message in context '{$contextName}'", __METHOD__ );
516 return false;
517 }
518 if ( $translation === null ) // No translation provided, then take the source as a reference
519 {
520 // eZDebug::writeError( "No translation, skipping message", __METHOD__ );
521 $translation = $source;
522 }
523 /* we need to convert ourselves if we're using libxml stuff here */
524 if ( $message instanceof DOMElement )
525 {
526 $codec = eZTextCodec::instance( "utf8" );
527 $source = $codec->convertString( $source );
528 $translation = $codec->convertString( $translation );
529 $comment = $codec->convertString( $comment );
530 }
531
532 $this->insert( $contextName, $source, $translation, $comment );
533 return true;
534 }
535
541 function findKey( $key )
542 {
543 $msg = null;
544 if ( isset( $this->Messages[$key] ) )
545 {
546 $msg = $this->Messages[$key];
547 }
548 return $msg;
549 }
550
558 function findMessage( $context, $source, $comment = null )
559 {
560 // First try with comment,
561 $man = eZTranslatorManager::instance();
562 $key = $man->createKey( $context, $source, $comment );
563
564 if ( !isset( $this->Messages[$key] ) )
565 {
566 // then try without comment for general translation
567 $key = $man->createKey( $context, $source );
568 }
569
570 return $this->findKey( $key );
571 }
572
578 function keyTranslate( $key )
579 {
580 $msg = $this->findKey( $key );
581 if ( $msg !== null )
582 return $msg["translation"];
583 else
584 {
585 return null;
586 }
587 }
588
596 function translate( $context, $source, $comment = null )
597 {
598 $msg = $this->findMessage( $context, $source, $comment );
599 if ( $msg !== null )
600 {
601 return $msg["translation"];
602 }
603
604 return null;
605 }
606
620 function insert( $context, $source, $translation, $comment = null )
621 {
622 if ( $context == "" )
623 $context = "default";
624 $man = eZTranslatorManager::instance();
625 $key = $man->createKey( $context, $source, $comment );
626 $msg = $man->createMessage( $context, $source, $comment, $translation );
627 $msg["key"] = $key;
628 $this->Messages[$key] = $msg;
629
630 // Set array of messages to be cached
631 if ( $this->UseCache == true && $this->BuildCache == true )
632 {
633 if ( !isset( $this->CachedMessages[$context] ) )
634 $this->CachedMessages[$context] = array();
635 $this->CachedMessages[$context][$key] = $msg;
636 }
637 return $key;
638 }
639
651 function remove( $context, $source, $message = null )
652 {
653 if ( $context == "" )
654 $context = "default";
655 $man = eZTranslatorManager::instance();
656 $key = $man->createKey( $context, $source, $message );
657 if ( isset( $this->Messages[$key] ) )
658 unset( $this->Messages[$key] );
659 }
660
668 function removeKey( $key )
669 {
670 if ( isset( $this->Messages[$key] ) )
671 unset( $this->Messages[$key] );
672 }
673
681 static function fetchList( $localeList = array() )
682 {
683 $ini = eZINI::instance();
684
685 $dir = $ini->variable( 'RegionalSettings', 'TranslationRepository' );
686
687 $fileInfoList = array();
688 $translationList = array();
689 $locale = '';
690
691 if ( count( $localeList ) == 0 )
692 {
693 $localeList = eZDir::findSubdirs( $dir );
694 }
695
696 foreach( $localeList as $locale )
697 {
698 if ( $locale != 'untranslated' )
699 {
700 $translationFiles = eZDir::findSubitems( $dir . '/' . $locale, 'f' );
701
702 foreach( $translationFiles as $translationFile )
703 {
704 if ( eZFile::suffix( $translationFile ) == 'ts' )
705 {
706 $translationList[] = new eZTSTranslator( $locale, $translationFile );
707 }
708 }
709 }
710 }
711
712 return $translationList;
713 }
714
719 static function resetGlobals()
720 {
721 unset( $GLOBALS['eZTSTranslationTables'] );
722 unset( $GLOBALS['eZTranslationCacheTable'] );
723 }
724
733 public static function expireCache( $timestamp = false, $locale = null )
734 {
735 if ( $timestamp === false )
736 $timestamp = time();
737
738 $handler = eZExpiryHandler::instance();
739 if ( $locale )
740 $handler->setTimestamp( self::EXPIRY_KEY . '-' . $locale, $timestamp );
741 else
742 $handler->setTimestamp( self::EXPIRY_KEY, $timestamp );
743 $handler->store();
744 self::resetGlobals();
745 }
746
751 public $Messages;
752 public $File;
753 public $UseCache;
754 public $BuildCache;
755 public $CachedMessages;
756 public $Locale;
757 public $HasRestoredCache;
758 public $RootCache;
759
763 const EXPIRY_KEY = 'ts-translation-cache';
764 }
765
766 ?>
eZDebugSetting\writeDebug
static writeDebug( $conditionName, $string, $label="")
Definition: ezdebugsetting.php:127
eZDebugSetting\writeNotice
static writeNotice( $conditionName, $string, $label="")
Definition: ezdebugsetting.php:85
eZDebug\writeError
static writeError( $string, $label="", $backgroundClass="")
Definition: ezdebug.php:545
eZDebug\createAccumulatorGroup
static createAccumulatorGroup( $key, $name=false)
Definition: ezdebug.php:1314
eZDebug\writeWarning
static writeWarning( $string, $label="", $backgroundClass="")
Definition: ezdebug.php:502
eZDebug\accumulatorStart
static accumulatorStart( $key, $inGroup=false, $name=false, $recursive=false)
Definition: ezdebug.php:1361
eZDebug\accumulatorStop
static accumulatorStop( $key, $recursive=false)
Definition: ezdebug.php:1389
eZDir\findSubitems
static findSubitems( $dir, $types=false, $fullPath=false, $includeHidden=false, $excludeItems=false)
Definition: ezdir.php:528
eZDir\findSubdirs
static findSubdirs( $dir, $includeHidden=false, $excludeItems=false)
Definition: ezdir.php:519
eZDir\path
static path( $names, $includeEndSeparator=false, $type=self::SEPARATOR_UNIX)
Definition: ezdir.php:237
eZExtension\baseDirectory
static baseDirectory(eZINI|null $siteINI=null)
Definition: ezextension.php:39
eZFile\suffix
static suffix( $filename)
Definition: ezfile.php:102
eZTSTranslator
Definition: eztstranslator.php:17
eZTSTranslator\handleMessageNode
handleMessageNode( $contextName, $message)
Definition: eztstranslator.php:448
eZTSTranslator\resetGlobals
static resetGlobals()
Definition: eztstranslator.php:719
eZTSTranslator\removeKey
removeKey( $key)
Definition: eztstranslator.php:668
eZTSTranslator\$Messages
$Messages
Definition: eztstranslator.php:751
eZTSTranslator\__construct
__construct( $locale, $filename=null, $useCache=true)
Definition: eztstranslator.php:24
eZTSTranslator\validateDOMTree
static validateDOMTree( $tree)
Definition: eztstranslator.php:371
eZTSTranslator\$Locale
$Locale
Definition: eztstranslator.php:756
eZTSTranslator\initialize
static initialize( $context, $locale, $filename, $useCache=true)
Definition: eztstranslator.php:53
eZTSTranslator\loadTranslationFile
loadTranslationFile( $locale, $filename, $requestedContext)
Definition: eztstranslator.php:115
eZTSTranslator\EXPIRY_KEY
const EXPIRY_KEY
Definition: eztstranslator.php:763
eZTSTranslator\$HasRestoredCache
$HasRestoredCache
Definition: eztstranslator.php:757
eZTSTranslator\findKey
findKey( $key)
Definition: eztstranslator.php:541
eZTSTranslator\$CachedMessages
$CachedMessages
Definition: eztstranslator.php:755
eZTSTranslator\handleContextNode
handleContextNode( $context)
Definition: eztstranslator.php:386
eZTSTranslator\expireCache
static expireCache( $timestamp=false, $locale=null)
Definition: eztstranslator.php:733
eZTSTranslator\insert
insert( $context, $source, $translation, $comment=null)
Definition: eztstranslator.php:620
eZTSTranslator\load
load( $requestedContext)
Definition: eztstranslator.php:97
eZTSTranslator\findMessage
findMessage( $context, $source, $comment=null)
Definition: eztstranslator.php:558
eZTSTranslator\$RootCache
$RootCache
Definition: eztstranslator.php:758
eZTSTranslator\hasInitializedContext
hasInitializedContext( $context)
Definition: eztstranslator.php:86
eZTSTranslator\fetchList
static fetchList( $localeList=array())
Definition: eztstranslator.php:681
eZTSTranslator\$File
$File
Definition: eztstranslator.php:752
eZTSTranslator\$UseCache
$UseCache
Definition: eztstranslator.php:753
eZTSTranslator\translate
translate( $context, $source, $comment=null)
Definition: eztstranslator.php:596
eZTSTranslator\$BuildCache
$BuildCache
Definition: eztstranslator.php:754
eZTSTranslator\keyTranslate
keyTranslate( $key)
Definition: eztstranslator.php:578
eZTextCodec\internalCharset
static internalCharset()
Definition: eztextcodec.php:546
eZTextCodec\instance
static instance( $inputCharsetCode, $outputCharsetCode=false, $alwaysReturn=true)
Definition: eztextcodec.php:446
eZTranslationCache\contextCache
static contextCache( $contextName)
Definition: eztranslationcache.php:39
eZTranslationCache\storeCache
static storeCache( $key)
Definition: eztranslationcache.php:173
eZTranslationCache\setContextCache
static setContextCache( $contextName, $context)
Definition: eztranslationcache.php:54
eZTranslationCache\canRestoreCache
static canRestoreCache( $key, $timestamp)
Definition: eztranslationcache.php:124
eZTranslationCache\restoreCache
static restoreCache( $key)
Definition: eztranslationcache.php:145
eZTranslatorHandler
Base class for translation handling.
Definition: eztranslatorhandler.php:19
eZTranslatorManager\instance
static instance()
Definition: eztranslatormanager.php:127
Copyright © 1998 – 2026 7x. All rights reserved. Licensed under the GNU GPLv2+.