PageRenderTime 88ms CodeModel.GetById 63ms app.highlight 19ms RepoModel.GetById 1ms app.codeStats 0ms

/system/classes/habarilocale.php

https://github.com/HabariMag/habarimag-old
PHP | 437 lines | 243 code | 46 blank | 148 comment | 38 complexity | e8eb82f5bfc03df9a238360cdd597b37 MD5 | raw file
  1<?php
  2/**
  3 * @package Habari
  4 *
  5 */
  6
  7/**
  8 * Habari Locale Class
  9 *
 10 * Provides translation services.
 11 *
 12 */
 13class HabariLocale
 14{
 15	private static $uselocale = false;
 16	private static $messages = array();
 17	private static $plural_function;
 18	private static $locale;
 19
 20	/**
 21	 * Sets the locale for Habari.
 22	 *
 23	 * @param string $locale A language code like 'en' or 'en-us' or 'x-klingon', will be lowercased
 24	 */
 25	public static function set( $locale = null )
 26	{
 27		if ( $locale == null ) {
 28			return;
 29		}
 30
 31		self::$locale = strtolower( $locale );
 32		self::$uselocale = self::load_domain( 'habari' );
 33	}
 34
 35	/**
 36	 * Set system locale.
 37	 *
 38	 * The problem is that every platform has its own way to designate a locale,
 39	 * so for German you could have 'de', 'de_DE', 'de_DE.UTF-8', 'de_DE.UTF-8@euro'
 40	 * (Linux) or 'DEU' (Windows), etc.
 41	 *
 42	 * @todo: This setting should probably be stored in the language files.
 43	 *
 44	 * @param string... $locale The locale(s) to set. They will be tried in order.
 45	 * @return string the locale that was picked, or false if an error occurred
 46	 */
 47	public static function set_system_locale()
 48	{
 49		if ( func_num_args() == 0 ) return;
 50		$args = func_get_args();
 51		array_unshift( $args, LC_ALL );
 52
 53		return call_user_func_array( 'setlocale', $args );
 54	}
 55
 56	/**
 57	 * Load translations for a given domain and base directory for a pluggable object.
 58	 * Translations are stored in gettext-style .mo files.
 59	 * The internal workings of the file format are not entirely meant to be understood.
 60	 *
 61	 * @link http://www.gnu.org/software/gettext/manual/html_node/gettext_136.html GNU Gettext Manual: Description of the MO file format
 62	 * @param string $domain the domain to load
 63	 * @param string $base_dir the base directory in which to find the translation files
 64	 * @return boolean true if data was successfully loaded, false otherwise
 65	 */
 66	public static function load_pluggable_domain( $domain, $base_dir )
 67	{
 68		$file = $base_dir . '/locale/' . self::$locale . '/LC_MESSAGES/' . $domain . '.mo';
 69		return self::load_file( $domain, $file );
 70	}
 71
 72	/**
 73	 * Load translations for a given domain.
 74	 * Translations are stored in gettext-style .mo files.
 75	 * The internal workings of the file format are not entirely meant to be understood.
 76	 *
 77	 * @link http://www.gnu.org/software/gettext/manual/html_node/gettext_136.html GNU Gettext Manual: Description of the MO file format
 78	 * @param string $domain the domain to load
 79	 * @return boolean true if data was successfully loaded, false otherwise
 80	 */
 81	private static function load_domain( $domain )
 82	{
 83		$file_end = self::$locale . '/LC_MESSAGES/' . $domain . '.mo';
 84
 85		if ( file_exists( Site::get_dir( 'config' ) . '/locale/' . $file_end ) ) {
 86			$file = Site::get_dir( 'config' ) . '/locale/' . $file_end;
 87		}
 88		else if ( file_exists( HABARI_PATH . '/user/locale/' . $file_end ) ) {
 89			$file = HABARI_PATH . '/user/locale/' . $file_end;
 90		}
 91		else if ( file_exists( HABARI_PATH . '/3rdparty/locale/' . $file_end ) ) {
 92			$file = HABARI_PATH . '/3rdparty/locale/' . $file_end;
 93		}
 94		else {
 95			$file = HABARI_PATH . '/system/locale/' . $file_end;
 96		}
 97
 98		return self::load_file( $domain, $file );
 99	}
100
101	/**
102	 * function list_all
103	 * Retrieves an array of the Habari locales that are installed
104	 *
105	 * @return array. An array of Habari locales in the installation
106	 */
107	public static function list_all()
108	{
109		$localedirs = array( HABARI_PATH . '/system/locale/', HABARI_PATH . '/3rdparty/locale/', HABARI_PATH . '/user/locale/' );
110		if ( Site::CONFIG_LOCAL != Site::$config_type ) {
111			// include site-specific locales
112			$localedirs[] = Site::get_dir( 'config' ) . '/locale/';
113		}
114
115		$dirs = array();
116		foreach ( $localedirs as $localedir ) {
117			if ( file_exists( $localedir ) ) {
118				$dirs = array_merge( $dirs, Utils::glob( $localedir . '*', GLOB_ONLYDIR | GLOB_MARK ) );
119			}
120		}
121		$dirs = array_filter( $dirs, create_function( '$a', 'return file_exists($a . "LC_MESSAGES/habari.mo");' ) );
122
123		$locales = array_map( 'basename', $dirs );
124		ksort( $locales );
125		return $locales;
126	}
127
128	/**
129	 * Load translations from a given file.
130	 *
131	 * @param string $domain the domain to load the data into
132	 * @param string $file the file name
133	 * @return boolean true if data was successfully loaded, false otherwise
134	 */
135	private static function load_file( $domain, $file )
136	{
137		if ( ! file_exists( $file ) ) {
138			Error::raise( sprintf( _t( 'No translations found for locale %s, domain %s!' ), self::$locale, $domain ) );
139			return false;
140		}
141		if ( filesize( $file ) < 24 ) {
142			Error::raise( sprintf( _t( 'Invalid .MO file for locale %s, domain %s!' ), self::$locale, $domain ) );
143			return false;
144		}
145
146		$fp = fopen( $file, 'rb' );
147		$data = fread( $fp, filesize( $file ) );
148		fclose( $fp );
149
150		// determine endianness
151		$little_endian = true;
152
153		list(,$magic) = unpack( 'V1', substr( $data, 0, 4 ) );
154		switch ( $magic & 0xFFFFFFFF ) {
155			case (int)0x950412de:
156				$little_endian = true;
157				break;
158			case (int)0xde120495:
159				$little_endian = false;
160				break;
161			default:
162				Error::raise( sprintf( _t( 'Invalid magic number 0x%08x in %s!' ), $magic, $file ) );
163				return false;
164		}
165
166		$revision = substr( $data, 4, 4 );
167		if ( $revision != 0 ) {
168			Error::raise( sprintf( _t( 'Unknown revision number %d in %s!' ), $revision, $file ) );
169			return false;
170		}
171
172		$l = $little_endian ? 'V' : 'N';
173
174		if ( $data && strlen( $data ) >= 20 ) {
175			$header = substr( $data, 8, 12 );
176			$header = unpack( "{$l}1msgcount/{$l}1msgblock/{$l}1transblock", $header );
177
178			if ( $header['msgblock'] + ($header['msgcount'] - 1 ) * 8 > filesize( $file ) ) {
179				Error::raise( sprintf( _t( 'Message count (%d) out of bounds in %s!' ), $header['msgcount'], $file ) );
180				return false;
181			}
182
183			$lo = "{$l}1length/{$l}1offset";
184
185			for ( $msgindex = 0; $msgindex < $header['msgcount']; $msgindex++ ) {
186				$msginfo = unpack( $lo, substr( $data, $header['msgblock'] + $msgindex * 8, 8 ) );
187				$msgids = explode( "\0", substr( $data, $msginfo['offset'], $msginfo['length'] ) );
188				$transinfo = unpack( $lo, substr( $data, $header['transblock'] + $msgindex * 8, 8 ) );
189				$transids = explode( "\0", substr( $data, $transinfo['offset'], $transinfo['length'] ) );
190				self::$messages[$domain][$msgids[0]] = array(
191					$msgids,
192					$transids,
193				);
194			}
195		}
196
197		// setup plural functionality
198		self::$plural_function = self::get_plural_function( self::$messages[$domain][''][1][0] );
199
200		// only use locale if we actually read something
201		return ( count( self::$messages ) > 0 );
202	}
203
204	private static function get_plural_function( $header )
205	{
206		if ( preg_match( '/plural-forms: (.*?)$/i', $header, $matches ) && preg_match( '/^\s*nplurals\s*=\s*(\d+)\s*;\s*plural=(.*)$/u', $matches[1], $matches ) ) {
207			// sanitize
208			$nplurals = preg_replace( '/[^0-9]/', '', $matches[1] );
209			$plural = preg_replace( '#[^n0-9:\(\)\?\|\&=!<>+*/\%-]#', '', $matches[2] );
210
211			$body = str_replace(
212				array( 'plural',  'n',  '$n$plurals', ),
213				array( '$plural', '$n', '$nplurals', ),
214				'nplurals='. $nplurals . '; plural=' . $plural
215			);
216
217			// add parens
218			// important since PHP's ternary evaluates from left to right
219			$body .= ';';
220			$res = '';
221			$p = 0;
222			for ( $i = 0; $i < strlen( $body ); $i++ ) {
223				$ch = $body[$i];
224				switch ( $ch ) {
225					case '?':
226						$res .= ' ? (';
227						$p++;
228						break;
229					case ':':
230						$res .= ') : (';
231						break;
232					case ';':
233						$res .= str_repeat( ')', $p ) . ';';
234						$p = 0;
235						break;
236					default:
237						$res .= $ch;
238				}
239			}
240
241			$body = $res . 'return ($plural>=$nplurals?$nplurals-1:$plural);';
242			$fn = create_function(
243				'$n',
244				$body
245			);
246		}
247		else {
248			// default: one plural form for all cases but n==1 (english)
249			$fn = create_function(
250				'$n',
251				'$nplurals=2;$plural=($n==1?0:1);return ($plural>=$nplurals?$nplurals-1:$plural);'
252			);
253		}
254
255		return $fn;
256	}
257
258	/**
259	 * DO NOT USE THIS FUNCTION.
260	 * This function is only to be used by the test case for the Locale class!
261	 */
262	public static function __run_plural_test( $header )
263	{
264		$fn = self::get_plural_function( $header );
265		$res = '';
266		for ( $n = 0; $n < 200; $n++ ) {
267			$res .= $fn( $n );
268		}
269
270		return $res;
271	}
272
273	/**
274	 * DO NOT USE THIS FUNCTION.
275	 * This function is only to be used by the test case for the Locale class!
276	 */
277	public static function __run_loadfile_test( $filename )
278	{
279		return self::load_file( 'test', $filename );
280	}
281
282	/**
283	 * Echo a version of the string translated into the current locale
284	 * @param string $text The text to echo translated
285	 * @param string $domain (optional) The domain to search for the message
286	 */
287	public static function _e()
288	{
289		$args = func_get_args();
290		echo call_user_func_array( array( 'HabariLocale', '_t' ), $args );
291	}
292
293	/**
294	 * Return a version of the string translated into the current locale
295	 *
296	 * @param string $text The text to echo translated
297	 * @param string $domain (optional) The domain to search for the message
298	 * @return string The translated string
299	 */
300	public static function _t( $text, $args = array(), $domain = 'habari' )
301	{
302		if ( is_string( $args ) ) {
303			$domain = $args;
304		}
305
306		if ( isset( self::$messages[$domain][$text] ) ) {
307			$t = self::$messages[$domain][$text][1][0];
308		}
309		else {
310			$t = $text;
311		}
312
313		if ( !empty( $args ) && is_array( $args ) ) {
314			array_unshift( $args, $t );
315			$t = call_user_func_array( 'sprintf', $args );
316		}
317
318		return $t;
319	}
320
321	/**
322	 * Given a string translated into the current locale, return the untranslated string.
323	 *
324	 * @param string $text The translated string
325	 * @param string $domain (optional) The domain to search for the message
326	 * @return string The untranslated string
327	 */
328	public static function _u( $text, $domain = 'habari' )
329	{
330		$t = $text;
331		foreach ( self::$messages[$domain] as $msg ) {
332			if ( $text == $msg[1][0] ) {
333				$t = $msg[0][0];
334				break;
335			}
336		}
337
338		return $t;
339	}
340	/**
341	 * Echo singular or plural version of the string, translated into the current locale, based on the count provided
342	 *
343	 * @param string $singular The singular form
344	 * @param string $plural The plural form
345	 * @param string $count The count
346	 * @param string $domain (optional) The domain to search for the message
347	 */
348	public static function _ne( $singular, $plural, $count, $domain = 'habari' )
349	{
350		echo self::_n( $singular, $plural, $count, $domain );
351	}
352
353	/**
354	 * Return a singular or plural string translated into the current locale based on the count provided
355	 *
356	 * @param string $singular The singular form
357	 * @param string $plural The plural form
358	 * @param string $count The count
359	 * @param string $domain (optional) The domain to search for the message
360	 * @return string The appropriately translated string
361	 */
362	public static function _n( $singular, $plural, $count, $domain = 'habari' )
363	{
364		if ( isset( self::$messages[$domain][$singular] ) ) {
365			// XXX workaround, but direct calling doesn't work
366			$fn = self::$plural_function;
367			$n = $fn( $count );
368			if ( isset( self::$messages[$domain][$singular][1][$n] ) ) {
369				return self::$messages[$domain][$singular][1][$n];
370			}
371		}
372		// fall-through else for both cases
373		return ( $count == 1 ? $singular : $plural );
374	}
375}
376
377/**
378 * Echo a version of the string translated into the current locale, alias for HabariLocale::_e()
379 *
380 * @param string $text The text to translate
381 */
382function _e( $text, $args = array(), $domain = 'habari' )
383{
384	return HabariLocale::_e( $text, $args, $domain );
385}
386
387/**
388 * function _ne
389 * Echo singular or plural version of the string, translated into the current locale, based on the count provided,
390 * alias for HabariLocale::_ne()
391 * @param string $singular The singular form
392 * @param string $plural The plural form
393 * @param string $count The count
394 */
395function _ne( $singular, $plural, $count, $domain = 'habari' )
396{
397	return HabariLocale::_ne( $singular, $plural, $count, $domain );
398}
399
400/**
401 * Return a version of the string translated into the current locale, alias for HabariLocale::_t()
402 *
403 * @param string $text The text to translate
404 * @return string The translated string
405 */
406function _t( $text, $args = array(), $domain = 'habari' )
407{
408	return HabariLocale::_t( $text, $args, $domain );
409}
410
411/**
412 * Return a singular or plural string translated into the current locale based on the count provided
413 *
414 * @param string $singular The singular form
415 * @param string $plural The plural form
416 * @param string $count The count
417 * @return string The appropriately translated string
418 */
419function _n( $singular, $plural, $count, $domain = 'habari' )
420{
421	return HabariLocale::_n( $singular, $plural, $count, $domain );
422}
423
424/**
425 * Given a string translated into the current locale, return the untranslated version of the string.
426 * Alias for HabariLocale::_u()
427 *
428 * @param string $text The translated string
429 * @param string $domain (optional) The domain to search for the message
430 * @return string The untranslated string
431 */
432function _u( $text, $domain = 'habari' )
433{
434	return HabariLocale::_u( $text, $domain );
435}
436
437?>