PageRenderTime 3ms CodeModel.GetById 28ms app.highlight 20ms RepoModel.GetById 1ms app.codeStats 0ms

/includes/lang.php

https://code.google.com/p/enanocms/
PHP | 790 lines | 469 code | 131 blank | 190 comment | 100 complexity | 46e6c0acebc66d20a8b0bb09b3d033af MD5 | raw file
  1<?php
  2
  3/*
  4 * Enano - an open-source CMS capable of wiki functions, Drupal-like sidebar blocks, and everything in between
  5 * Copyright (C) 2006-2009 Dan Fuhry
  6 *
  7 * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
  8 * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
  9 *
 10 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 11 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
 12 */
 13
 14/**
 15 * Language class - processes, stores, and retrieves language strings.
 16 * @package Enano
 17 * @subpackage Localization
 18 * @copyright 2007 Dan Fuhry
 19 * @license GNU General Public License
 20 */
 21
 22class Language
 23{
 24	
 25	/**
 26 	* The numerical ID of the loaded language.
 27 	* @var int
 28 	*/
 29	
 30	var $lang_id;
 31	
 32	/**
 33 	* The ISO-639-3 code for the loaded language. This should be grabbed directly from the database.
 34 	* @var string
 35 	*/
 36	
 37	var $lang_code;
 38
 39	/**
 40 	* Used to track when a language was last changed, to allow browsers to cache language data
 41 	* @var int
 42 	*/
 43	
 44	var $lang_timestamp;
 45	
 46	/**
 47 	* Will be an object that holds an instance of the class configured with the site's default language. Only instanciated when needed.
 48 	* @var object
 49 	*/
 50	
 51	var $default;
 52	
 53	/**
 54 	* The list of loaded strings.
 55 	* @var array
 56 	* @access private
 57 	*/
 58	
 59	var $strings = array();
 60	
 61	/**
 62 	* Switch for debug mode. If true, will show an asterisk after localized strings. This
 63 	* can be useful if you're localizing a component and need to see what's already done.
 64 	* @var bool
 65 	*/
 66	
 67	var $debug = false;
 68	
 69	/**
 70 	* List of available filters to pass variables through.
 71 	* @var array
 72 	* @access private
 73 	*/
 74	
 75	protected $filters = array();
 76	
 77	/**
 78 	* Constructor.
 79 	* @param int|string Language ID or code to load.
 80 	*/
 81	
 82	function __construct($lang)
 83	{
 84		global $db, $session, $paths, $template, $plugins; // Common objects
 85		
 86		if ( defined('IN_ENANO_INSTALL') && ( !defined('ENANO_CONFIG_FETCHED') || ( defined('IN_ENANO_UPGRADE') && !defined('IN_ENANO_UPGRADE_POST') ) ) )
 87		{
 88			// special case for the Enano installer: it will load its own strings from a JSON file and just use this API for fetching
 89			// and templatizing them.
 90			// 1.1.4 fix: this was still being called after main API startup from installer payload
 91			$this->lang_id   = 1;
 92			$this->lang_code = $lang;
 93			return true;
 94		}
 95		if ( is_string($lang) )
 96		{
 97			$sql_col = 'lang_code=\'' . $db->escape($lang) . '\'';
 98		}
 99		else if ( is_int($lang) )
100		{
101			$sql_col = 'lang_id=' . $lang . '';
102		}
103		else
104		{
105			$db->_die('lang.php - attempting to pass invalid value to constructor');
106		}
107		
108		$lang_default = ( $x = getConfig('default_language') ) ? intval($x) : '0';
109		
110		$q = $db->sql_query("SELECT lang_id, lang_code, last_changed, ( lang_id = $lang_default ) AS is_default FROM " . table_prefix . "language WHERE $sql_col OR lang_id = $lang_default ORDER BY is_default ASC LIMIT 1;");
111		
112		if ( !$q )
113			$db->_die('lang.php - main select query');
114		
115		if ( $db->numrows() < 1 )
116			$db->_die('lang.php - There are no languages installed');
117		
118		$row = $db->fetchrow();
119		
120		$this->lang_id   = intval( $row['lang_id'] );
121		$this->lang_code = $row['lang_code'];
122		$this->lang_timestamp = $row['last_changed'];
123		
124		$this->register_filter('htmlsafe', 'htmlspecialchars');
125		$this->register_filter('urlencode', 'urlencode');
126		$this->register_filter('rawurlencode', 'rawurlencode');
127		
128		$code = $plugins->setHook('lang_init');
129		foreach ( $code as $cmd )
130		{
131			eval($cmd);
132		}
133	}
134	
135	/**
136 	* Fetches language strings from the database, or a cache file if it's available.
137 	* @param bool If true (default), allows the cache to be used.
138 	*/
139	
140	function fetch($allow_cache = true)
141	{
142		global $db, $session, $paths, $template, $plugins; // Common objects
143		
144		// Attempt to load the strings from a cache file
145		$loaded = false;
146		
147		if ( $allow_cache )
148		{
149			// Load the cache manager
150			global $cache;
151			
152			if ( $cached = $cache->fetch("lang_{$this->lang_id}") )
153			{
154				$this->merge($cached);
155				$loaded = true;
156			}
157		}
158		if ( !$loaded )
159		{
160			// No cache file - select and retrieve from the database
161			$q = $db->sql_unbuffered_query("SELECT string_category, string_name, string_content FROM " . table_prefix . "language_strings WHERE lang_id = {$this->lang_id};");
162			if ( !$q )
163				$db->_die('lang.php - selecting language string data');
164			if ( $row = $db->fetchrow() )
165			{
166				$strings = array();
167				do
168				{
169					$cat =& $row['string_category'];
170					if ( !is_array(@$strings[$cat]) )
171					{
172						$strings[$cat] = array();
173					}
174					$strings[$cat][ $row['string_name'] ] = $row['string_content'];
175				}
176				while ( $row = $db->fetchrow() );
177				// all done fetching
178				$this->merge($strings);
179				$this->regen_caches(false);
180			}
181			/*
182			else
183			{
184				if ( !defined('ENANO_ALLOW_LOAD_NOLANG') )
185					$db->_die('lang.php - No strings for language ' . $this->lang_code);
186			}
187			*/
188		}
189	}
190	
191	/**
192 	* Loads a file from the disk cache (treated as PHP) and merges it into RAM.
193 	* @param string File to load
194 	*/
195	
196	function load_cache_file($file)
197	{
198		global $db, $session, $paths, $template, $plugins; // Common objects
199		
200		if ( !file_exists($file) )
201			$db->_die('lang.php - requested cache file doesn\'t exist');
202		
203		@include($file);
204				
205		if ( !isset($lang_cache) || ( isset($lang_cache) && !is_array($lang_cache) ) )
206			$db->_die('lang.php - the cache file is invalid (didn\'t set $lang_cache as an array)');
207		
208		$this->merge($lang_cache);
209	}
210	
211	/**
212 	* Loads a JSON language file and parses the strings into RAM. Will use the cache if possible, but stays far away from the database,
213 	* which we assume doesn't exist yet.
214 	*/
215	
216	function load_file($file)
217	{
218		global $db, $session, $paths, $template, $plugins; // Common objects
219		
220		if ( !file_exists($file) )
221		{
222			if ( defined('IN_ENANO_INSTALL') )
223			{
224				die('lang.php - requested JSON file (' . htmlspecialchars($file) . ') doesn\'t exist');
225			}
226			else
227			{
228				$db->_die('lang.php - requested JSON file doesn\'t exist');
229			}
230		}
231		
232		$contents = trim(@file_get_contents($file));
233		if ( empty($contents) )
234			$db->_die('lang.php - empty language file...');
235		
236		// Trim off all text before and after the starting and ending braces
237		$contents = preg_replace('/^([^{]+)\{/', '{', $contents);
238		$contents = preg_replace('/\}([^}]+)$/', '}', $contents);
239		$contents = trim($contents);
240		
241		if ( empty($contents) )
242			$db->_die('lang.php - no meat to the language file...');
243		
244		$checksum = md5($contents);
245		if ( file_exists("./cache/lang_json_{$checksum}.php") )
246		{
247			$this->load_cache_file("./cache/lang_json_{$checksum}.php");
248		}
249		else
250		{
251			// Correct syntax to be nice to the json parser
252		
253			// eliminate comments
254			$contents = preg_replace(array(
255							// eliminate single line comments in '// ...' form
256							'#^\s*//(.+)$#m',
257							// eliminate multi-line comments in '/* ... */' form, at start of string
258							'#^\s*/\*(.+)\*/#Us',
259							// eliminate multi-line comments in '/* ... */' form, at end of string
260							'#/\*(.+)\*/\s*$#Us'
261						), '', $contents);
262			
263			$contents = preg_replace('/([,\{\[])([\s]*?)([a-z0-9_]+)([\s]*?):/', '\\1\\2"\\3" :', $contents);
264			
265			try
266			{
267				$langdata = enano_json_decode($contents);
268			}
269			catch(Zend_Json_Exception $e)
270			{
271				$db->_die('lang.php - Exception caught by JSON parser</p><pre>' . htmlspecialchars(print_r($e, true)) . '</pre><p>');
272				exit;
273			}
274		
275			if ( !is_array($langdata) )
276				$db->_die('lang.php - invalid language file');
277			
278			if ( !isset($langdata['categories']) || !isset($langdata['strings']) )
279				$db->_die('lang.php - language file does not contain the proper items');
280			
281			$this->merge($langdata['strings']);
282			
283			$lang_file = "./cache/lang_json_{$checksum}.php";
284			
285			$handle = @fopen($lang_file, 'w');
286			if ( !$handle )
287				// Couldn't open the file. Silently fail and let the strings come from RAM.
288				return false;
289				
290			// The file's open, that means we should be good.
291			fwrite($handle, '<?php
292// This file was generated automatically by Enano. You should not edit this file because any changes you make
293// to it will not be visible in the ACP and all changes will be lost upon any changes to strings in the admin panel.
294
295$lang_cache = ');
296			
297			$exported = $this->var_export_string($this->strings);
298			if ( empty($exported) )
299				// Ehh, that's not good
300				$db->_die('lang.php - load_file(): var_export_string() failed');
301			
302			fwrite($handle, $exported . '; ?>');
303			
304			// Clean up
305			unset($exported, $langdata);
306			
307			// Done =)
308			fclose($handle);
309		}
310	}
311	
312	/**
313 	* Merges a standard language assoc array ($arr[cat][stringid]) with the master in RAM.
314 	* @param array
315 	*/
316	
317	function merge($strings)
318	{
319		// This is stupidly simple.
320		foreach ( $strings as $cat_id => $contents )
321		{
322			if ( !isset($this->strings[$cat_id]) || ( isset($this->strings[$cat_id]) && !is_array($this->strings[$cat_id]) ) )
323				$this->strings[$cat_id] = array();
324			foreach ( $contents as $string_id => $string )
325			{
326				$this->strings[$cat_id][$string_id] = $string;
327			}
328		}
329	}
330	
331	/**
332 	* Imports a JSON-format language file into the database and merges with current strings.
333 	* @param string Path to the JSON file to load
334 	* @param bool If true, only imports new strings and skips those that already exist. Defaults to false (import all strings)
335 	* @param bool Enable debugging output, makes the process over CLI more interesting
336 	*/
337	
338	function import($file, $skip_existing = false, $debug = false)
339	{
340		global $db, $session, $paths, $template, $plugins; // Common objects
341		
342		if ( !file_exists($file) )
343			$db->_die('lang.php - can\'t import language file: string file doesn\'t exist');
344		
345		if ( $this->lang_id == 0 )
346			$db->_die('lang.php - BUG: trying to perform import when $lang->lang_id == 0');
347		
348		if ( $debug )
349			$br = ( isset($_SERVER['REQUEST_URI']) ) ? '<br />' : '';
350		
351		if ( $debug )
352			echo "Importing file: $file$br\n  Checking file...$br\n";
353		
354		$contents = trim(@file_get_contents($file));
355		
356		if ( empty($contents) )
357			$db->_die('lang.php - can\'t load the contents of the language file');
358		
359		if ( $debug )
360			echo "  Cleaning up JSON$br\n";
361		
362		// Trim off all text before and after the starting and ending braces
363		$contents = preg_replace('/^([^{]+)\{/', '{', $contents);
364		$contents = preg_replace('/\}([^}]+)$/', '}', $contents);
365		
366		// Correct syntax to be nice to the json parser
367		$contents = enano_clean_json($contents);
368		
369		if ( $debug )
370			echo "  Decoding JSON stream$br\n";
371		
372		try
373		{
374			$langdata = enano_json_decode($contents);
375		}
376		catch(Zend_Json_Exception $e)
377		{
378			$db->_die('lang.php - Exception caught by JSON parser</p><pre>' . htmlspecialchars(print_r($e, true)) . '</pre><p>');
379			exit;
380		}
381		
382		if ( !is_array($langdata) )
383		{
384			$db->_die('lang.php - invalid or non-well-formed language file');
385		}
386		
387		if ( $debug )
388			echo "  Starting string import$br\n";
389		
390		return $this->import_array($langdata, $skip_existing, $debug);
391	}
392	
393	/**
394 	* Imports a JSON-format language file into the database and merges with current strings.
395 	* @param string Path to plugin file
396 	*/
397	
398	function import_plugin($file)
399	{
400		global $db, $session, $paths, $template, $plugins; // Common objects
401		
402		if ( !file_exists($file) )
403			$db->_die('lang.php - can\'t import language file: string file doesn\'t exist');
404		
405		if ( $this->lang_id == 0 )
406			$db->_die('lang.php - BUG: trying to perform import when $lang->lang_id == 0');
407		
408		$block = pluginLoader::parse_plugin_blocks($file, 'language');
409		if ( !is_array($block) )
410			return false;
411		if ( !isset($block[0]) )
412			return false;
413		
414		$contents =& $block[0]['value'];
415		
416		// Trim off all text before and after the starting and ending braces
417		$contents = enano_trim_json($contents);
418		
419		// Correct syntax to be nice to the json parser
420		$contents = enano_clean_json($contents);
421		
422		try
423		{
424			$langdata = enano_json_decode($contents);
425		}
426		catch(Zend_Json_Exception $e)
427		{
428			$db->_die('lang.php - Exception caught by JSON parser</p><pre>' . htmlspecialchars(print_r($e, true)) . '</pre><p>');
429			exit;
430		}
431		
432		if ( !is_array($langdata) )
433		{
434			$db->_die('lang.php - invalid or non-well-formed language file');
435		}
436		
437		// Does the plugin support the current language?
438		if ( isset($langdata[$this->lang_code]) )
439		{
440			// Yes, import that
441			return $this->import_array($langdata[$this->lang_code]);
442		}
443		
444		// Just import the first language we run across.
445		$supported_langs = array_keys($langdata);
446		
447		if ( !isset($supported_langs[0]) )
448		{
449			$db->_die('lang.php - plugin has an invalid or corrupt language block');
450		}
451		
452		$first_lang = $supported_langs[0];
453		
454		return $this->import_array($langdata[$first_lang], false, true);
455	}
456	
457	/**
458 	* Performs the actual import of string data.
459 	* @param array Parsed JSON object, should be in the form of an array
460 	* @param bool If true, only imports new strings and skips those that already exist. Defaults to false.
461 	* @param bool Enable debugging output
462 	* @access private
463 	*/
464	
465	protected function import_array($langdata, $skip_existing = false, $debug = false) 
466	{
467		global $db, $session, $paths, $template, $plugins; // Common objects
468		
469		if ( !isset($langdata['categories']) || !isset($langdata['strings']) )
470			$db->_die('lang.php - language file does not contain the proper items');
471		
472		if ( $debug )
473			$br = ( isset($_SERVER['REQUEST_URI']) ) ? '<br />' : '';
474		
475		$insert_list = array();
476		$delete_list = array();
477		
478		foreach ( $langdata['categories'] as $category )
479		{
480			if ( isset($langdata['strings'][$category]) )
481			{
482				if ( $debug )
483				{
484					$desc = ( isset($langdata['strings']['meta'][$category]) ) ? $langdata['strings']['meta'][$category] : $this->get("meta_$category");
485					echo "  Indexing category: $category ({$desc})$br\n";
486				}
487				foreach ( $langdata['strings'][$category] as $string_name => $string_value )
488				{
489					// should we skip this string?
490					if ( isset($this->strings[$category]) && $skip_existing )
491					{
492						if ( isset($this->strings[$category][$string_name]) )
493						{
494							// already exists, skip
495							if ( $debug )
496								echo "    Skipping string (already exists): {$category}_{$string_name}$br\n";
497							continue;
498						}
499					}
500					$string_name = $db->escape($string_name);
501					$string_value = $db->escape($string_value);
502					$category_name = $db->escape($category);
503					$insert_list[] = "({$this->lang_id}, '$category_name', '$string_name', '$string_value')";
504					$delete_list[] = "( lang_id = {$this->lang_id} AND string_category = '$category_name' AND string_name = '$string_name' )";
505				}
506			}
507		}
508		
509		if ( $debug )
510		{
511			echo "  Running deletion of old strings...";
512			$start = microtime_float();
513		}
514		$delete_list = implode(" OR\n  ", $delete_list);
515		
516		if ( !empty($delete_list) )
517		{
518			$sql = "DELETE FROM " . table_prefix . "language_strings WHERE $delete_list;";
519			
520			// Free some memory
521			unset($delete_list);
522			
523			// Run the query
524			$q = $db->sql_query($sql);
525			if ( !$q )
526				$db->_die('lang.php - couldn\'t kill off them old strings');
527		}
528		
529		if ( $debug )
530		{
531			$time = round(microtime_float() - $start, 5);
532			echo "({$time}s)$br\n";
533		}
534		
535		if ( !empty($insert_list) )
536		{
537			if ( $debug )
538			{
539				echo "  Inserting strings...";
540				$start = microtime_float();
541			}
542			$insert_list = implode(",\n  ", $insert_list);
543			$sql = "INSERT INTO " . table_prefix . "language_strings(lang_id, string_category, string_name, string_content) VALUES\n  $insert_list;";
544			
545			// Free some memory
546			unset($insert_list);
547			
548			// Run the query
549			$q = $db->sql_query($sql);
550			if ( !$q )
551				$db->_die('lang.php - couldn\'t insert strings in import()');
552			
553			if ( $debug )
554			{
555				$time = round(microtime_float() - $start, 5);
556				echo "({$time}s)$br\n";
557			}
558		}
559		
560		// YAY! done!
561		// This will regenerate the cache file if possible.
562		if ( $debug )
563			echo "  Regenerating cache file$br\n";
564		$this->regen_caches();
565		
566		return true;
567	}
568	
569	/**
570 	* Refetches the strings and writes out the cache file.
571 	*/
572	
573	function regen_caches($refetch = true)
574	{
575		global $db, $session, $paths, $template, $plugins; // Common objects
576		
577		// Refresh the strings in RAM to the latest copies in the DB
578		if ( $refetch )
579			$this->fetch(false);
580		
581		// Load the cache manager
582		global $cache;
583		
584		// Store it
585		$cache->store("lang_{$this->lang_id}", $this->strings, -1);
586		
587		// Update timestamp in database
588		$q = $db->sql_query('UPDATE ' . table_prefix . 'language SET last_changed = ' . time() . ' WHERE lang_id = ' . $this->lang_id . ';');
589		if ( !$q )
590			$db->_die('lang.php - updating timestamp on language');
591		
592		return true;
593	}
594	
595	/**
596 	* Calls var_export() on whatever, and returns the function's output.
597 	* @param mixed Whatever you want var_exported. Usually an array.
598 	* @return string
599 	*/
600	
601	static function var_export_string($val)
602	{
603		/*
604		ob_start();
605		var_export($val);
606		$contents = ob_get_contents();
607		ob_end_clean();
608		return $contents;
609		*/
610		// Which PHP version was the second parameter added in?? o_O
611		return var_export($val, true);
612	}
613	
614	/**
615 	* Registers a filter, a function that strings can be passed through to change the string somehow (e.g. htmlspecialchars)
616 	* @param string Filter name. Lowercase alphanumeric (htmlsafe)
617 	* @param callback Function to call.
618 	* @return bool True on success, false if some error occurred
619 	*/
620	
621	public function register_filter($filter_name, $filter_function)
622	{
623		if ( !is_string($filter_function) && !is_array($filter_function) )
624		{
625			return false;
626		}
627		if ( ( is_string($filter_function) && !function_exists($filter_function) ) || ( is_array($filter_function) && !method_exists(@$filter_function[0], @$filter_function[1]) ) )
628		{
629			return false;
630		}
631		if ( !preg_match('/^[a-z0-9_]+$/', $filter_name) )
632		{
633			return false;
634		}
635		$this->filters[$filter_name] = $filter_function;
636		return true;
637	}
638	
639	/**
640 	* Fetches a language string from the cache in RAM. If it isn't there, it will call fetch() again and then try. If it still can't find it, it will ask for the string
641 	* in the default language. If even then the string can't be found, this function will return what was passed to it.
642 	*
643 	* This will also templatize strings. If a string contains variables in the format %foo%, you may specify the second parameter as an associative array in the format
644 	* of 'foo' => 'foo substitute'.
645 	*
646 	* @param string ID of the string to fetch. This will always be in the format of category_stringid.
647 	* @param array Optional. Associative array of substitutions.
648 	* @return string
649 	*/
650	
651	function get($string_id, $substitutions = false)
652	{
653		if ( !is_array($substitutions) )
654			$substitutions = array();
655		// if this isn't a valid language string ID, just return the string unprocessed.
656		if ( !preg_match('/^([a-z0-9]+)((_[a-z0-9]+)+)$/', $string_id) )
657			return $string_id;
658		return $this->substitute($this->get_uncensored($string_id), $substitutions);
659	}
660	
661	/**
662 	* The same as get(), but does not perform any substitution or filtering. Used in get() (of course) and in the admin panel, where
663 	* strings are updated only if they were changed.
664 	*
665 	* @param string ID of the string to fetch. This will always be in the format of category_stringid.
666 	* @param array Optional. Associative array of substitutions.
667 	* @return string
668 	*/
669	
670	function get_uncensored($string_id, $substitutions = false)
671	{
672		global $db, $session, $paths, $template, $plugins; // Common objects
673		
674		// Extract the category and string ID
675		$category = substr($string_id, 0, ( strpos($string_id, '_') ));
676		$string_name = substr($string_id, ( strpos($string_id, '_') + 1 ));
677		$found = false;
678		if ( isset($this->strings[$category]) && isset($this->strings[$category][$string_name]) )
679		{
680			$found = true;
681			$string = $this->strings[$category][$string_name];
682		}
683		if ( !$found )
684		{
685			// Ehh, the string wasn't found. Rerun fetch() and try again.
686			// Or if it's the installer, no use in refetching, so just fail.
687			if ( defined('IN_ENANO_INSTALL') || ( defined('ENANO_INSTALLED') && !@$db->_conn ) )
688			{
689				return $string_id;
690			}
691			$this->fetch();
692			profiler_log('Language(' . $this->lang_code . '): refetched due to missing string: ' . $string_id);
693			if ( isset($this->strings[$category]) && isset($this->strings[$category][$string_name]) )
694			{
695				$found = true;
696				$string = $this->strings[$category][$string_name];
697			}
698			if ( !$found )
699			{
700				// STILL not found. Check the default language.
701				$lang_default = ( $x = getConfig('default_language') ) ? intval($x) : $this->lang_id;
702				if ( $lang_default != $this->lang_id )
703				{
704					if ( !is_object($this->default) )
705						$this->default = new Language($lang_default);
706					return $this->default->get_uncensored($string_id);
707				}
708			}
709		}
710		if ( !$found )
711		{
712			// Alright, it's nowhere. Return the input, grumble grumble...
713			return $string_id;
714		}
715		// Found it!
716		return $string;
717	}
718	
719	/**
720 	* Processes substitutions.
721 	* @param string
722 	* @param array
723 	* @return string
724 	*/
725	
726	function substitute($string, $subs)
727	{
728		preg_match_all('/%this\.([a-z0-9_]+)((?:\|(?:[a-z0-9_]+))*)%/', $string, $matches);
729		if ( count($matches[0]) > 0 )
730		{
731			foreach ( $matches[1] as $i => $string_id )
732			{
733				$result = $this->get($string_id);
734				$string = str_replace($matches[0][$i], $this->process_filters($result, $matches[2][$i]), $string);
735			}
736		}
737		preg_match_all('/%config\.([a-z0-9_]+)((?:\|(?:[a-z0-9_]+))*)%/', $string, $matches);
738		if ( count($matches[0]) > 0 )
739		{
740			foreach ( $matches[1] as $i => $string_id )
741			{
742				$result = getConfig($string_id, '');
743				$string = str_replace($matches[0][$i], $this->process_filters($result, $matches[2][$i]), $string);
744			}
745		}
746		preg_match_all('/%([a-z0-9_]+)((?:\|(?:[a-z0-9_]+))*)%/', $string, $matches);
747		if ( count($matches[0]) > 0 )
748		{
749			foreach ( $matches[1] as $i => $string_id )
750			{
751				if ( isset($subs[$string_id]) )
752				{
753					$string = str_replace($matches[0][$i], $this->process_filters($subs[$string_id], $matches[2][$i]), $string);
754				}
755			}
756		}
757		return ( $this->debug ) ? "$string*" : $string;
758	}
759	
760	/**
761 	* Processes filters to a language string.
762 	* @param string Unprocessed string
763 	* @param string Filter list (format: |filter1|filter2|filter3, initial pipe is important); can also be an array if you so desire
764 	* @return string
765 	*/
766	
767	function process_filters($string, $filters)
768	{
769		if ( !empty($filters) )
770		{
771			$filters = trim($filters, '|');
772			$filters = explode('|', $filters);
773			foreach ( $filters as $filter )
774			{
775				if ( isset($this->filters[$filter]) )
776				{
777					$result = call_user_func($this->filters[$filter], $string);
778					if ( is_string($result) )
779					{
780						$string = $result;
781					}
782				}
783			}
784		}
785		return $string;
786	}
787	
788} // class Language
789
790?>