PageRenderTime 74ms CodeModel.GetById 38ms app.highlight 18ms RepoModel.GetById 12ms app.codeStats 0ms

/src/Joomla/Language/Language.php

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