PageRenderTime 28ms CodeModel.GetById 2ms app.highlight 18ms RepoModel.GetById 1ms app.codeStats 1ms

/libraries/joomla/language/language.php

https://github.com/J2MTecnologia/joomla-3.x
PHP | 1376 lines | 822 code | 139 blank | 415 comment | 79 complexity | 95ffbe4b56baace77afe292e1767f000 MD5 | raw file
   1<?php
   2/**
   3 * @package     Joomla.Platform
   4 * @subpackage  Language
   5 *
   6 * @copyright   Copyright (C) 2005 - 2014 Open Source Matters, Inc. All rights reserved.
   7 * @license     GNU General Public License version 2 or later; see LICENSE
   8 */
   9
  10defined('JPATH_PLATFORM') or die;
  11
  12/**
  13 * Allows for quoting in language .ini files.
  14 */
  15define('_QQ_', '"');
  16
  17/**
  18 * Languages/translation handler class
  19 *
  20 * @package     Joomla.Platform
  21 * @subpackage  Language
  22 * @since       11.1
  23 */
  24class JLanguage
  25{
  26	/**
  27	 * Array of JLanguage objects
  28	 *
  29	 * @var    array
  30	 * @since  11.1
  31	 */
  32	protected static $languages = array();
  33
  34	/**
  35	 * Debug language, If true, highlights if string isn't found.
  36	 *
  37	 * @var    boolean
  38	 * @since  11.1
  39	 */
  40	protected $debug = false;
  41
  42	/**
  43	 * The default language, used when a language file in the requested language does not exist.
  44	 *
  45	 * @var    string
  46	 * @since  11.1
  47	 */
  48	protected $default = 'en-GB';
  49
  50	/**
  51	 * An array of orphaned text.
  52	 *
  53	 * @var    array
  54	 * @since  11.1
  55	 */
  56	protected $orphans = array();
  57
  58	/**
  59	 * Array holding the language metadata.
  60	 *
  61	 * @var    array
  62	 * @since  11.1
  63	 */
  64	protected $metadata = null;
  65
  66	/**
  67	 * Array holding the language locale or boolean null if none.
  68	 *
  69	 * @var    array|boolean
  70	 * @since  11.1
  71	 */
  72	protected $locale = null;
  73
  74	/**
  75	 * The language to load.
  76	 *
  77	 * @var    string
  78	 * @since  11.1
  79	 */
  80	protected $lang = null;
  81
  82	/**
  83	 * A nested array of language files that have been loaded
  84	 *
  85	 * @var    array
  86	 * @since  11.1
  87	 */
  88	protected $paths = array();
  89
  90	/**
  91	 * List of language files that are in error state
  92	 *
  93	 * @var    array
  94	 * @since  11.1
  95	 */
  96	protected $errorfiles = array();
  97
  98	/**
  99	 * Translations
 100	 *
 101	 * @var    array
 102	 * @since  11.1
 103	 */
 104	protected $strings = array();
 105
 106	/**
 107	 * An array of used text, used during debugging.
 108	 *
 109	 * @var    array
 110	 * @since  11.1
 111	 */
 112	protected $used = array();
 113
 114	/**
 115	 * Counter for number of loads.
 116	 *
 117	 * @var    integer
 118	 * @since  11.1
 119	 */
 120	protected $counter = 0;
 121
 122	/**
 123	 * An array used to store overrides.
 124	 *
 125	 * @var    array
 126	 * @since  11.1
 127	 */
 128	protected $override = array();
 129
 130	/**
 131	 * Name of the transliterator function for this language.
 132	 *
 133	 * @var    string
 134	 * @since  11.1
 135	 */
 136	protected $transliterator = null;
 137
 138	/**
 139	 * Name of the pluralSuffixesCallback function for this language.
 140	 *
 141	 * @var    callable
 142	 * @since  11.1
 143	 */
 144	protected $pluralSuffixesCallback = null;
 145
 146	/**
 147	 * Name of the ignoredSearchWordsCallback function for this language.
 148	 *
 149	 * @var    callable
 150	 * @since  11.1
 151	 */
 152	protected $ignoredSearchWordsCallback = null;
 153
 154	/**
 155	 * Name of the lowerLimitSearchWordCallback function for this language.
 156	 *
 157	 * @var    callable
 158	 * @since  11.1
 159	 */
 160	protected $lowerLimitSearchWordCallback = null;
 161
 162	/**
 163	 * Name of the uppperLimitSearchWordCallback function for this language.
 164	 *
 165	 * @var    callable
 166	 * @since  11.1
 167	 */
 168	protected $upperLimitSearchWordCallback = null;
 169
 170	/**
 171	 * Name of the searchDisplayedCharactersNumberCallback function for this language.
 172	 *
 173	 * @var    callable
 174	 * @since  11.1
 175	 */
 176	protected $searchDisplayedCharactersNumberCallback = null;
 177
 178	/**
 179	 * Constructor activating the default information of the language.
 180	 *
 181	 * @param   string   $lang   The language
 182	 * @param   boolean  $debug  Indicates if language debugging is enabled.
 183	 *
 184	 * @since   11.1
 185	 */
 186	public function __construct($lang = null, $debug = false)
 187	{
 188		$this->strings = array();
 189
 190		if ($lang == null)
 191		{
 192			$lang = $this->default;
 193		}
 194
 195		$this->setLanguage($lang);
 196		$this->setDebug($debug);
 197
 198		$filename = JPATH_BASE . "/language/overrides/$lang.override.ini";
 199
 200		if (file_exists($filename) && $contents = $this->parse($filename))
 201		{
 202			if (is_array($contents))
 203			{
 204				// Sort the underlying heap by key values to optimize merging
 205				ksort($contents, SORT_STRING);
 206				$this->override = $contents;
 207			}
 208
 209			unset($contents);
 210		}
 211
 212		// Look for a language specific localise class
 213		$class = str_replace('-', '_', $lang . 'Localise');
 214		$paths = array();
 215
 216		if (defined('JPATH_SITE'))
 217		{
 218			// Note: Manual indexing to enforce load order.
 219			$paths[0] = JPATH_SITE . "/language/overrides/$lang.localise.php";
 220			$paths[2] = JPATH_SITE . "/language/$lang/$lang.localise.php";
 221		}
 222
 223		if (defined('JPATH_ADMINISTRATOR'))
 224		{
 225			// Note: Manual indexing to enforce load order.
 226			$paths[1] = JPATH_ADMINISTRATOR . "/language/overrides/$lang.localise.php";
 227			$paths[3] = JPATH_ADMINISTRATOR . "/language/$lang/$lang.localise.php";
 228		}
 229
 230		ksort($paths);
 231		$path = reset($paths);
 232
 233		while (!class_exists($class) && $path)
 234		{
 235			if (file_exists($path))
 236			{
 237				require_once $path;
 238			}
 239
 240			$path = next($paths);
 241		}
 242
 243		if (class_exists($class))
 244		{
 245			/* Class exists. Try to find
 246			 * -a transliterate method,
 247			 * -a getPluralSuffixes method,
 248			 * -a getIgnoredSearchWords method
 249			 * -a getLowerLimitSearchWord method
 250			 * -a getUpperLimitSearchWord method
 251			 * -a getSearchDisplayCharactersNumber method
 252			 */
 253			if (method_exists($class, 'transliterate'))
 254			{
 255				$this->transliterator = array($class, 'transliterate');
 256			}
 257
 258			if (method_exists($class, 'getPluralSuffixes'))
 259			{
 260				$this->pluralSuffixesCallback = array($class, 'getPluralSuffixes');
 261			}
 262
 263			if (method_exists($class, 'getIgnoredSearchWords'))
 264			{
 265				$this->ignoredSearchWordsCallback = array($class, 'getIgnoredSearchWords');
 266			}
 267
 268			if (method_exists($class, 'getLowerLimitSearchWord'))
 269			{
 270				$this->lowerLimitSearchWordCallback = array($class, 'getLowerLimitSearchWord');
 271			}
 272
 273			if (method_exists($class, 'getUpperLimitSearchWord'))
 274			{
 275				$this->upperLimitSearchWordCallback = array($class, 'getUpperLimitSearchWord');
 276			}
 277
 278			if (method_exists($class, 'getSearchDisplayedCharactersNumber'))
 279			{
 280				$this->searchDisplayedCharactersNumberCallback = array($class, 'getSearchDisplayedCharactersNumber');
 281			}
 282		}
 283
 284		$this->load();
 285	}
 286
 287	/**
 288	 * Returns a language object.
 289	 *
 290	 * @param   string   $lang   The language to use.
 291	 * @param   boolean  $debug  The debug mode.
 292	 *
 293	 * @return  JLanguage  The Language object.
 294	 *
 295	 * @since   11.1
 296	 */
 297	public static function getInstance($lang, $debug = false)
 298	{
 299		if (!isset(self::$languages[$lang . $debug]))
 300		{
 301			self::$languages[$lang . $debug] = new JLanguage($lang, $debug);
 302		}
 303
 304		return self::$languages[$lang . $debug];
 305	}
 306
 307	/**
 308	 * Translate function, mimics the php gettext (alias _) function.
 309	 *
 310	 * The function checks if $jsSafe is true, then if $interpretBackslashes is true.
 311	 *
 312	 * @param   string   $string                The string to translate
 313	 * @param   boolean  $jsSafe                Make the result javascript safe
 314	 * @param   boolean  $interpretBackSlashes  Interpret \t and \n
 315	 *
 316	 * @return  string  The translation of the string
 317	 *
 318	 * @since   11.1
 319	 */
 320	public function _($string, $jsSafe = false, $interpretBackSlashes = true)
 321	{
 322		// Detect empty string
 323		if ($string == '')
 324		{
 325			return '';
 326		}
 327
 328		$key = strtoupper($string);
 329
 330		if (isset($this->strings[$key]))
 331		{
 332			$string = $this->debug ? '**' . $this->strings[$key] . '**' : $this->strings[$key];
 333
 334			// Store debug information
 335			if ($this->debug)
 336			{
 337				$caller = $this->getCallerInfo();
 338
 339				if (!array_key_exists($key, $this->used))
 340				{
 341					$this->used[$key] = array();
 342				}
 343
 344				$this->used[$key][] = $caller;
 345			}
 346		}
 347		else
 348		{
 349			if ($this->debug)
 350			{
 351				$caller = $this->getCallerInfo();
 352				$caller['string'] = $string;
 353
 354				if (!array_key_exists($key, $this->orphans))
 355				{
 356					$this->orphans[$key] = array();
 357				}
 358
 359				$this->orphans[$key][] = $caller;
 360
 361				$string = '??' . $string . '??';
 362			}
 363		}
 364
 365		if ($jsSafe)
 366		{
 367			// Javascript filter
 368			$string = addslashes($string);
 369		}
 370		elseif ($interpretBackSlashes)
 371		{
 372			// Interpret \n and \t characters
 373			$string = str_replace(array('\\\\', '\t', '\n'), array("\\", "\t", "\n"), $string);
 374		}
 375
 376		return $string;
 377	}
 378
 379	/**
 380	 * Transliterate function
 381	 *
 382	 * This method processes a string and replaces all accented UTF-8 characters by unaccented
 383	 * ASCII-7 "equivalents".
 384	 *
 385	 * @param   string  $string  The string to transliterate.
 386	 *
 387	 * @return  string  The transliteration of the string.
 388	 *
 389	 * @since   11.1
 390	 */
 391	public function transliterate($string)
 392	{
 393		if ($this->transliterator !== null)
 394		{
 395			return call_user_func($this->transliterator, $string);
 396		}
 397
 398		$string = JLanguageTransliterate::utf8_latin_to_ascii($string);
 399		$string = JString::strtolower($string);
 400
 401		return $string;
 402	}
 403
 404	/**
 405	 * Getter for transliteration function
 406	 *
 407	 * @return  callable  The transliterator function
 408	 *
 409	 * @since   11.1
 410	 */
 411	public function getTransliterator()
 412	{
 413		return $this->transliterator;
 414	}
 415
 416	/**
 417	 * Set the transliteration function.
 418	 *
 419	 * @param   callable  $function  Function name or the actual function.
 420	 *
 421	 * @return  callable  The previous function.
 422	 *
 423	 * @since   11.1
 424	 */
 425	public function setTransliterator($function)
 426	{
 427		$previous = $this->transliterator;
 428		$this->transliterator = $function;
 429
 430		return $previous;
 431	}
 432
 433	/**
 434	 * Returns an array of suffixes for plural rules.
 435	 *
 436	 * @param   integer  $count  The count number the rule is for.
 437	 *
 438	 * @return  array    The array of suffixes.
 439	 *
 440	 * @since   11.1
 441	 */
 442	public function getPluralSuffixes($count)
 443	{
 444		if ($this->pluralSuffixesCallback !== null)
 445		{
 446			return call_user_func($this->pluralSuffixesCallback, $count);
 447		}
 448		else
 449		{
 450			return array((string) $count);
 451		}
 452	}
 453
 454	/**
 455	 * Getter for pluralSuffixesCallback function.
 456	 *
 457	 * @return  callable  Function name or the actual function.
 458	 *
 459	 * @since   11.1
 460	 */
 461	public function getPluralSuffixesCallback()
 462	{
 463		return $this->pluralSuffixesCallback;
 464	}
 465
 466	/**
 467	 * Set the pluralSuffixes function.
 468	 *
 469	 * @param   callable  $function  Function name or actual function.
 470	 *
 471	 * @return  callable  The previous function.
 472	 *
 473	 * @since   11.1
 474	 */
 475	public function setPluralSuffixesCallback($function)
 476	{
 477		$previous = $this->pluralSuffixesCallback;
 478		$this->pluralSuffixesCallback = $function;
 479
 480		return $previous;
 481	}
 482
 483	/**
 484	 * Returns an array of ignored search words
 485	 *
 486	 * @return  array  The array of ignored search words.
 487	 *
 488	 * @since   11.1
 489	 */
 490	public function getIgnoredSearchWords()
 491	{
 492		if ($this->ignoredSearchWordsCallback !== null)
 493		{
 494			return call_user_func($this->ignoredSearchWordsCallback);
 495		}
 496		else
 497		{
 498			return array();
 499		}
 500	}
 501
 502	/**
 503	 * Getter for ignoredSearchWordsCallback function.
 504	 *
 505	 * @return  callable  Function name or the actual function.
 506	 *
 507	 * @since   11.1
 508	 */
 509	public function getIgnoredSearchWordsCallback()
 510	{
 511		return $this->ignoredSearchWordsCallback;
 512	}
 513
 514	/**
 515	 * Setter for the ignoredSearchWordsCallback function
 516	 *
 517	 * @param   callable  $function  Function name or actual function.
 518	 *
 519	 * @return  callable  The previous function.
 520	 *
 521	 * @since   11.1
 522	 */
 523	public function setIgnoredSearchWordsCallback($function)
 524	{
 525		$previous = $this->ignoredSearchWordsCallback;
 526		$this->ignoredSearchWordsCallback = $function;
 527
 528		return $previous;
 529	}
 530
 531	/**
 532	 * Returns a lower limit integer for length of search words
 533	 *
 534	 * @return  integer  The lower limit integer for length of search words (3 if no value was set for a specific language).
 535	 *
 536	 * @since   11.1
 537	 */
 538	public function getLowerLimitSearchWord()
 539	{
 540		if ($this->lowerLimitSearchWordCallback !== null)
 541		{
 542			return call_user_func($this->lowerLimitSearchWordCallback);
 543		}
 544		else
 545		{
 546			return 3;
 547		}
 548	}
 549
 550	/**
 551	 * Getter for lowerLimitSearchWordCallback function
 552	 *
 553	 * @return  callable  Function name or the actual function.
 554	 *
 555	 * @since   11.1
 556	 */
 557	public function getLowerLimitSearchWordCallback()
 558	{
 559		return $this->lowerLimitSearchWordCallback;
 560	}
 561
 562	/**
 563	 * Setter for the lowerLimitSearchWordCallback function.
 564	 *
 565	 * @param   callable  $function  Function name or actual function.
 566	 *
 567	 * @return  callable  The previous function.
 568	 *
 569	 * @since   11.1
 570	 */
 571	public function setLowerLimitSearchWordCallback($function)
 572	{
 573		$previous = $this->lowerLimitSearchWordCallback;
 574		$this->lowerLimitSearchWordCallback = $function;
 575
 576		return $previous;
 577	}
 578
 579	/**
 580	 * Returns an upper limit integer for length of search words
 581	 *
 582	 * @return  integer  The upper limit integer for length of search words (20 if no value was set for a specific language).
 583	 *
 584	 * @since   11.1
 585	 */
 586	public function getUpperLimitSearchWord()
 587	{
 588		if ($this->upperLimitSearchWordCallback !== null)
 589		{
 590			return call_user_func($this->upperLimitSearchWordCallback);
 591		}
 592		else
 593		{
 594			return 20;
 595		}
 596	}
 597
 598	/**
 599	 * Getter for upperLimitSearchWordCallback function
 600	 *
 601	 * @return  callable  Function name or the actual function.
 602	 *
 603	 * @since   11.1
 604	 */
 605	public function getUpperLimitSearchWordCallback()
 606	{
 607		return $this->upperLimitSearchWordCallback;
 608	}
 609
 610	/**
 611	 * Setter for the upperLimitSearchWordCallback function
 612	 *
 613	 * @param   callable  $function  Function name or the actual function.
 614	 *
 615	 * @return  callable  The previous function.
 616	 *
 617	 * @since   11.1
 618	 */
 619	public function setUpperLimitSearchWordCallback($function)
 620	{
 621		$previous = $this->upperLimitSearchWordCallback;
 622		$this->upperLimitSearchWordCallback = $function;
 623
 624		return $previous;
 625	}
 626
 627	/**
 628	 * Returns the number of characters displayed in search results.
 629	 *
 630	 * @return  integer  The number of characters displayed (200 if no value was set for a specific language).
 631	 *
 632	 * @since   11.1
 633	 */
 634	public function getSearchDisplayedCharactersNumber()
 635	{
 636		if ($this->searchDisplayedCharactersNumberCallback !== null)
 637		{
 638			return call_user_func($this->searchDisplayedCharactersNumberCallback);
 639		}
 640		else
 641		{
 642			return 200;
 643		}
 644	}
 645
 646	/**
 647	 * Getter for searchDisplayedCharactersNumberCallback function
 648	 *
 649	 * @return  callable  Function name or the actual function.
 650	 *
 651	 * @since   11.1
 652	 */
 653	public function getSearchDisplayedCharactersNumberCallback()
 654	{
 655		return $this->searchDisplayedCharactersNumberCallback;
 656	}
 657
 658	/**
 659	 * Setter for the searchDisplayedCharactersNumberCallback function.
 660	 *
 661	 * @param   callable  $function  Function name or the actual function.
 662	 *
 663	 * @return  callable  The previous function.
 664	 *
 665	 * @since   11.1
 666	 */
 667	public function setSearchDisplayedCharactersNumberCallback($function)
 668	{
 669		$previous = $this->searchDisplayedCharactersNumberCallback;
 670		$this->searchDisplayedCharactersNumberCallback = $function;
 671
 672		return $previous;
 673	}
 674
 675	/**
 676	 * Checks if a language exists.
 677	 *
 678	 * This is a simple, quick check for the directory that should contain language files for the given user.
 679	 *
 680	 * @param   string  $lang      Language to check.
 681	 * @param   string  $basePath  Optional path to check.
 682	 *
 683	 * @return  boolean  True if the language exists.
 684	 *
 685	 * @since   11.1
 686	 */
 687	public static function exists($lang, $basePath = JPATH_BASE)
 688	{
 689		static $paths = array();
 690
 691		// Return false if no language was specified
 692		if (!$lang)
 693		{
 694			return false;
 695		}
 696
 697		$path = $basePath . '/language/' . $lang;
 698
 699		// Return previous check results if it exists
 700		if (isset($paths[$path]))
 701		{
 702			return $paths[$path];
 703		}
 704
 705		// Check if the language exists
 706		$paths[$path] = is_dir($path);
 707
 708		return $paths[$path];
 709	}
 710
 711	/**
 712	 * Loads a single language file and appends the results to the existing strings
 713	 *
 714	 * @param   string   $extension  The extension for which a language file should be loaded.
 715	 * @param   string   $basePath   The basepath to use.
 716	 * @param   string   $lang       The language to load, default null for the current language.
 717	 * @param   boolean  $reload     Flag that will force a language to be reloaded if set to true.
 718	 * @param   boolean  $default    Flag that force the default language to be loaded if the current does not exist.
 719	 *
 720	 * @return  boolean  True if the file has successfully loaded.
 721	 *
 722	 * @since   11.1
 723	 */
 724	public function load($extension = 'joomla', $basePath = JPATH_BASE, $lang = null, $reload = false, $default = true)
 725	{
 726		// Load the default language first if we're not debugging and a non-default language is requested to be loaded
 727		// with $default set to true
 728		if (!$this->debug && ($lang != $this->default) && $default)
 729		{
 730			$this->load($extension, $basePath, $this->default, false, true);
 731		}
 732
 733		if (!$lang)
 734		{
 735			$lang = $this->lang;
 736		}
 737
 738		$path = self::getLanguagePath($basePath, $lang);
 739
 740		$internal = $extension == 'joomla' || $extension == '';
 741		$filename = $internal ? $lang : $lang . '.' . $extension;
 742		$filename = "$path/$filename.ini";
 743
 744		if (isset($this->paths[$extension][$filename]) && !$reload)
 745		{
 746			// This file has already been tested for loading.
 747			$result = $this->paths[$extension][$filename];
 748		}
 749		else
 750		{
 751			// Load the language file
 752			$result = $this->loadLanguage($filename, $extension);
 753
 754			// Check whether there was a problem with loading the file
 755			if ($result === false && $default)
 756			{
 757				// No strings, so either file doesn't exist or the file is invalid
 758				$oldFilename = $filename;
 759
 760				// Check the standard file name
 761				$path = self::getLanguagePath($basePath, $this->default);
 762				$filename = $internal ? $this->default : $this->default . '.' . $extension;
 763				$filename = "$path/$filename.ini";
 764
 765				// If the one we tried is different than the new name, try again
 766				if ($oldFilename != $filename)
 767				{
 768					$result = $this->loadLanguage($filename, $extension, false);
 769				}
 770			}
 771		}
 772
 773		return $result;
 774	}
 775
 776	/**
 777	 * Loads a language file.
 778	 *
 779	 * This method will not note the successful loading of a file - use load() instead.
 780	 *
 781	 * @param   string  $filename   The name of the file.
 782	 * @param   string  $extension  The name of the extension.
 783	 *
 784	 * @return  boolean  True if new strings have been added to the language
 785	 *
 786	 * @see     JLanguage::load()
 787	 * @since   11.1
 788	 */
 789	protected function loadLanguage($filename, $extension = 'unknown')
 790	{
 791		$this->counter++;
 792
 793		$result = false;
 794		$strings = false;
 795
 796		if (file_exists($filename))
 797		{
 798			$strings = $this->parse($filename);
 799		}
 800
 801		if ($strings)
 802		{
 803			if (is_array($strings))
 804			{
 805				// Sort the underlying heap by key values to optimize merging
 806				ksort($strings, SORT_STRING);
 807				$this->strings = array_merge($this->strings, $strings);
 808			}
 809
 810			if (is_array($strings) && count($strings))
 811			{
 812				// Do not bother with ksort here.  Since the originals were sorted, PHP will already have chosen the best heap.
 813				$this->strings = array_merge($this->strings, $this->override);
 814				$result = true;
 815			}
 816		}
 817
 818		// Record the result of loading the extension's file.
 819		if (!isset($this->paths[$extension]))
 820		{
 821			$this->paths[$extension] = array();
 822		}
 823
 824		$this->paths[$extension][$filename] = $result;
 825
 826		return $result;
 827	}
 828
 829	/**
 830	 * Parses a language file.
 831	 *
 832	 * @param   string  $filename  The name of the file.
 833	 *
 834	 * @return  array  The array of parsed strings.
 835	 *
 836	 * @since   11.1
 837	 */
 838	protected function parse($filename)
 839	{
 840		if ($this->debug)
 841		{
 842			// Capture hidden PHP errors from the parsing.
 843			$php_errormsg = null;
 844			$track_errors = ini_get('track_errors');
 845			ini_set('track_errors', true);
 846		}
 847
 848		$contents = file_get_contents($filename);
 849		$contents = str_replace('_QQ_', '"\""', $contents);
 850		$strings = @parse_ini_string($contents);
 851
 852		if (!is_array($strings))
 853		{
 854			$strings = array();
 855		}
 856
 857		if ($this->debug)
 858		{
 859			// Restore error tracking to what it was before.
 860			ini_set('track_errors', $track_errors);
 861
 862			// Initialise variables for manually parsing the file for common errors.
 863			$blacklist = array('YES', 'NO', 'NULL', 'FALSE', 'ON', 'OFF', 'NONE', 'TRUE');
 864			$regex = '/^(|(\[[^\]]*\])|([A-Z][A-Z0-9_\-\.]*\s*=(\s*(("[^"]*")|(_QQ_)))+))\s*(;.*)?$/';
 865			$this->debug = false;
 866			$errors = array();
 867
 868			// Open the file as a stream.
 869			$file = new SplFileObject($filename);
 870
 871			foreach ($file as $lineNumber => $line)
 872			{
 873				// Avoid BOM error as BOM is OK when using parse_ini
 874				if ($lineNumber == 0)
 875				{
 876					$line = str_replace("\xEF\xBB\xBF", '', $line);
 877				}
 878
 879				// Check that the key is not in the blacklist and that the line format passes the regex.
 880				$key = strtoupper(trim(substr($line, 0, strpos($line, '='))));
 881
 882				// Workaround to reduce regex complexity when matching escaped quotes
 883				$line = str_replace('\"', '_QQ_', $line);
 884
 885				if (!preg_match($regex, $line) || in_array($key, $blacklist))
 886				{
 887					$errors[] = $lineNumber;
 888				}
 889			}
 890
 891			// Check if we encountered any errors.
 892			if (count($errors))
 893			{
 894				if (basename($filename) != $this->lang . '.ini')
 895				{
 896					$this->errorfiles[$filename] = $filename . JText::sprintf('JERROR_PARSING_LANGUAGE_FILE', implode(', ', $errors));
 897				}
 898				else
 899				{
 900					$this->errorfiles[$filename] = $filename . '&#160;: error(s) in line(s) ' . implode(', ', $errors);
 901				}
 902			}
 903			elseif ($php_errormsg)
 904			{
 905				// We didn't find any errors but there's probably a parse notice.
 906				$this->errorfiles['PHP' . $filename] = 'PHP parser errors :' . $php_errormsg;
 907			}
 908
 909			$this->debug = true;
 910		}
 911
 912		return $strings;
 913	}
 914
 915	/**
 916	 * Get a metadata language property.
 917	 *
 918	 * @param   string  $property  The name of the property.
 919	 * @param   mixed   $default   The default value.
 920	 *
 921	 * @return  mixed  The value of the property.
 922	 *
 923	 * @since   11.1
 924	 */
 925	public function get($property, $default = null)
 926	{
 927		if (isset($this->metadata[$property]))
 928		{
 929			return $this->metadata[$property];
 930		}
 931
 932		return $default;
 933	}
 934
 935	/**
 936	 * Determine who called JLanguage or JText.
 937	 *
 938	 * @return  array  Caller information.
 939	 *
 940	 * @since   11.1
 941	 */
 942	protected function getCallerInfo()
 943	{
 944		// Try to determine the source if none was provided
 945		if (!function_exists('debug_backtrace'))
 946		{
 947			return null;
 948		}
 949
 950		$backtrace = debug_backtrace();
 951		$info = array();
 952
 953		// Search through the backtrace to our caller
 954		$continue = true;
 955
 956		while ($continue && next($backtrace))
 957		{
 958			$step = current($backtrace);
 959			$class = @ $step['class'];
 960
 961			// We're looking for something outside of language.php
 962			if ($class != 'JLanguage' && $class != 'JText')
 963			{
 964				$info['function'] = @ $step['function'];
 965				$info['class'] = $class;
 966				$info['step'] = prev($backtrace);
 967
 968				// Determine the file and name of the file
 969				$info['file'] = @ $step['file'];
 970				$info['line'] = @ $step['line'];
 971
 972				$continue = false;
 973			}
 974		}
 975
 976		return $info;
 977	}
 978
 979	/**
 980	 * Getter for Name.
 981	 *
 982	 * @return  string  Official name element of the language.
 983	 *
 984	 * @since   11.1
 985	 */
 986	public function getName()
 987	{
 988		return $this->metadata['name'];
 989	}
 990
 991	/**
 992	 * Get a list of language files that have been loaded.
 993	 *
 994	 * @param   string  $extension  An optional extension name.
 995	 *
 996	 * @return  array
 997	 *
 998	 * @since   11.1
 999	 */
1000	public function getPaths($extension = null)
1001	{
1002		if (isset($extension))
1003		{
1004			if (isset($this->paths[$extension]))
1005			{
1006				return $this->paths[$extension];
1007			}
1008
1009			return null;
1010		}
1011		else
1012		{
1013			return $this->paths;
1014		}
1015	}
1016
1017	/**
1018	 * Get a list of language files that are in error state.
1019	 *
1020	 * @return  array
1021	 *
1022	 * @since   11.1
1023	 */
1024	public function getErrorFiles()
1025	{
1026		return $this->errorfiles;
1027	}
1028
1029	/**
1030	 * Getter for the language tag (as defined in RFC 3066)
1031	 *
1032	 * @return  string  The language tag.
1033	 *
1034	 * @since   11.1
1035	 */
1036	public function getTag()
1037	{
1038		return $this->metadata['tag'];
1039	}
1040
1041	/**
1042	 * Get the RTL property.
1043	 *
1044	 * @return  boolean  True is it an RTL language.
1045	 *
1046	 * @since   11.1
1047	 */
1048	public function isRTL()
1049	{
1050		return (bool) $this->metadata['rtl'];
1051	}
1052
1053	/**
1054	 * Set the Debug property.
1055	 *
1056	 * @param   boolean  $debug  The debug setting.
1057	 *
1058	 * @return  boolean  Previous value.
1059	 *
1060	 * @since   11.1
1061	 */
1062	public function setDebug($debug)
1063	{
1064		$previous = $this->debug;
1065		$this->debug = (boolean) $debug;
1066
1067		return $previous;
1068	}
1069
1070	/**
1071	 * Get the Debug property.
1072	 *
1073	 * @return  boolean  True is in debug mode.
1074	 *
1075	 * @since   11.1
1076	 */
1077	public function getDebug()
1078	{
1079		return $this->debug;
1080	}
1081
1082	/**
1083	 * Get the default language code.
1084	 *
1085	 * @return  string  Language code.
1086	 *
1087	 * @since   11.1
1088	 */
1089	public function getDefault()
1090	{
1091		return $this->default;
1092	}
1093
1094	/**
1095	 * Set the default language code.
1096	 *
1097	 * @param   string  $lang  The language code.
1098	 *
1099	 * @return  string  Previous value.
1100	 *
1101	 * @since   11.1
1102	 */
1103	public function setDefault($lang)
1104	{
1105		$previous = $this->default;
1106		$this->default = $lang;
1107
1108		return $previous;
1109	}
1110
1111	/**
1112	 * Get the list of orphaned strings if being tracked.
1113	 *
1114	 * @return  array  Orphaned text.
1115	 *
1116	 * @since   11.1
1117	 */
1118	public function getOrphans()
1119	{
1120		return $this->orphans;
1121	}
1122
1123	/**
1124	 * Get the list of used strings.
1125	 *
1126	 * Used strings are those strings requested and found either as a string or a constant.
1127	 *
1128	 * @return  array  Used strings.
1129	 *
1130	 * @since   11.1
1131	 */
1132	public function getUsed()
1133	{
1134		return $this->used;
1135	}
1136
1137	/**
1138	 * Determines is a key exists.
1139	 *
1140	 * @param   string  $string  The key to check.
1141	 *
1142	 * @return  boolean  True, if the key exists.
1143	 *
1144	 * @since   11.1
1145	 */
1146	public function hasKey($string)
1147	{
1148		$key = strtoupper($string);
1149
1150		return isset($this->strings[$key]);
1151	}
1152
1153	/**
1154	 * Returns a associative array holding the metadata.
1155	 *
1156	 * @param   string  $lang  The name of the language.
1157	 *
1158	 * @return  mixed  If $lang exists return key/value pair with the language metadata, otherwise return NULL.
1159	 *
1160	 * @since   11.1
1161	 */
1162	public static function getMetadata($lang)
1163	{
1164		$path = self::getLanguagePath(JPATH_BASE, $lang);
1165		$file = $lang . '.xml';
1166
1167		$result = null;
1168
1169		if (is_file("$path/$file"))
1170		{
1171			$result = self::parseXMLLanguageFile("$path/$file");
1172		}
1173
1174		if (empty($result))
1175		{
1176			return null;
1177		}
1178
1179		return $result;
1180	}
1181
1182	/**
1183	 * Returns a list of known languages for an area
1184	 *
1185	 * @param   string  $basePath  The basepath to use
1186	 *
1187	 * @return  array  key/value pair with the language file and real name.
1188	 *
1189	 * @since   11.1
1190	 */
1191	public static function getKnownLanguages($basePath = JPATH_BASE)
1192	{
1193		$dir = self::getLanguagePath($basePath);
1194		$knownLanguages = self::parseLanguageFiles($dir);
1195
1196		return $knownLanguages;
1197	}
1198
1199	/**
1200	 * Get the path to a language
1201	 *
1202	 * @param   string  $basePath  The basepath to use.
1203	 * @param   string  $language  The language tag.
1204	 *
1205	 * @return  string  language related path or null.
1206	 *
1207	 * @since   11.1
1208	 */
1209	public static function getLanguagePath($basePath = JPATH_BASE, $language = null)
1210	{
1211		$dir = $basePath . '/language';
1212
1213		if (!empty($language))
1214		{
1215			$dir .= '/' . $language;
1216		}
1217
1218		return $dir;
1219	}
1220
1221	/**
1222	 * Set the language attributes to the given language.
1223	 *
1224	 * Once called, the language still needs to be loaded using JLanguage::load().
1225	 *
1226	 * @param   string  $lang  Language code.
1227	 *
1228	 * @return  string  Previous value.
1229	 *
1230	 * @since   11.1
1231	 */
1232	public function setLanguage($lang)
1233	{
1234		$previous = $this->lang;
1235		$this->lang = $lang;
1236		$this->metadata = $this->getMetadata($this->lang);
1237
1238		return $previous;
1239	}
1240
1241	/**
1242	 * Get the language locale based on current language.
1243	 *
1244	 * @return  array  The locale according to the language.
1245	 *
1246	 * @since   11.1
1247	 */
1248	public function getLocale()
1249	{
1250		if (!isset($this->locale))
1251		{
1252			$locale = str_replace(' ', '', isset($this->metadata['locale']) ? $this->metadata['locale'] : '');
1253
1254			if ($locale)
1255			{
1256				$this->locale = explode(',', $locale);
1257			}
1258			else
1259			{
1260				$this->locale = false;
1261			}
1262		}
1263
1264		return $this->locale;
1265	}
1266
1267	/**
1268	 * Get the first day of the week for this language.
1269	 *
1270	 * @return  integer  The first day of the week according to the language
1271	 *
1272	 * @since   11.1
1273	 */
1274	public function getFirstDay()
1275	{
1276		return (int) (isset($this->metadata['firstDay']) ? $this->metadata['firstDay'] : 0);
1277	}
1278
1279	/**
1280	 * Get the weekends days for this language.
1281	 *
1282	 * @return  string  The weekend days of the week separated by a comma according to the language
1283	 *
1284	 * @since   3.2
1285	 */
1286	public function getWeekEnd()
1287	{
1288		return (isset($this->metadata['weekEnd']) && $this->metadata['weekEnd']) ? $this->metadata['weekEnd'] : '0,6';
1289	}
1290
1291	/**
1292	 * Searches for language directories within a certain base dir.
1293	 *
1294	 * @param   string  $dir  directory of files.
1295	 *
1296	 * @return  array  Array holding the found languages as filename => real name pairs.
1297	 *
1298	 * @since   11.1
1299	 */
1300	public static function parseLanguageFiles($dir = null)
1301	{
1302		$languages = array();
1303
1304		$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
1305
1306		foreach ($iterator as $file)
1307		{
1308			$langs    = array();
1309			$fileName = $file->getFilename();
1310
1311			if (!$file->isFile() || !preg_match("/^([-_A-Za-z]*)\.xml$/", $fileName))
1312			{
1313				continue;
1314			}
1315
1316			try
1317			{
1318				$metadata = self::parseXMLLanguageFile($file->getRealPath());
1319
1320				if ($metadata)
1321				{
1322					$lang = str_replace('.xml', '', $fileName);
1323					$langs[$lang] = $metadata;
1324				}
1325
1326				$languages = array_merge($languages, $langs);
1327			}
1328			catch (RuntimeException $e)
1329			{
1330			}
1331		}
1332
1333		return $languages;
1334	}
1335
1336	/**
1337	 * Parse XML file for language information.
1338	 *
1339	 * @param   string  $path  Path to the XML files.
1340	 *
1341	 * @return  array  Array holding the found metadata as a key => value pair.
1342	 *
1343	 * @since   11.1
1344	 * @throws  RuntimeException
1345	 */
1346	public static function parseXMLLanguageFile($path)
1347	{
1348		if (!is_readable($path))
1349		{
1350			throw new RuntimeException('File not found or not readable');
1351		}
1352
1353		// Try to load the file
1354		$xml = simplexml_load_file($path);
1355
1356		if (!$xml)
1357		{
1358			return null;
1359		}
1360
1361		// Check that it's a metadata file
1362		if ((string) $xml->getName() != 'metafile')
1363		{
1364			return null;
1365		}
1366
1367		$metadata = array();
1368
1369		foreach ($xml->metadata->children() as $child)
1370		{
1371			$metadata[$child->getName()] = (string) $child;
1372		}
1373
1374		return $metadata;
1375	}
1376}