PageRenderTime 54ms CodeModel.GetById 2ms app.highlight 44ms RepoModel.GetById 1ms app.codeStats 1ms

/src/Joomla/Language/Language.php

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