1: <?php
2: /**
3: * This file contains the the I18N class.
4: *
5: * @package Core
6: * @subpackage I18N
7: * @version SVN Revision $Rev:$
8: *
9: * @author Murat Purc <murat@purc.de>
10: * @copyright four for business AG <www.4fb.de>
11: * @license http://www.contenido.org/license/LIZENZ.txt
12: * @link http://www.4fb.de
13: * @link http://www.contenido.org
14: */
15:
16: defined('CON_FRAMEWORK') || die('Illegal call: Missing framework initialization - request aborted.');
17:
18: /**
19: * Internationalization (i18n) class.
20: *
21: * @package Core
22: * @subpackage I18N
23: */
24: class cI18n {
25:
26: /**
27: * i18n related assoziative data cache.
28: *
29: * @var array
30: */
31: protected static $_i18nData = array(
32: 'language' => NULL,
33: 'domains' => array(),
34: 'files' => array(),
35: 'cache' => array()
36: );
37:
38: /**
39: * Initializes the i18n.
40: *
41: * @param string $localePath Path to the locales
42: * @param string $langCode Language code to set
43: * @param string $domain Language domain
44: */
45: public static function init($localePath, $langCode, $domain = 'contenido') {
46: if (function_exists('bindtextdomain')) {
47: // Bind the domain 'contenido' to our locale path
48: bindtextdomain($domain, $localePath);
49:
50: // Set the default text domain to 'contenido'
51: textdomain($domain);
52:
53: // Half brute-force to set the locale.
54: if (!ini_get('safe_mode')) {
55: putenv("LANG=$langCode");
56: }
57:
58: if (defined('LC_MESSAGES')) {
59: setlocale(LC_MESSAGES, $langCode);
60: }
61:
62: setlocale(LC_CTYPE, $langCode);
63: }
64:
65: self::$_i18nData['domains'][$domain] = $localePath;
66: self::$_i18nData['language'] = $langCode;
67: }
68:
69: /**
70: * Returns translation of a specific text, wrapper for translate().
71: *
72: * @param string $string The string to translate
73: * @param string $domain The domain to look up
74: * @return string Returns the translation
75: */
76: public static function __($string, $domain = 'contenido') {
77: return self::translate($string, $domain);
78: }
79:
80: /**
81: * Returns translation of a specific text
82: *
83: * @param string $string The string to translate
84: * @param string $domain The domain to look up
85: * @throws cException if this is the backend mode and the $belang is not set
86: * @return string Returns the translation
87: */
88: public static function translate($string, $domain = 'contenido') {
89: global $cfg, $belang, $contenido;
90:
91: // Auto initialization
92: if (!self::$_i18nData['language']) {
93: if (!isset($belang)) {
94: if ($contenido) {
95: throw new cException('init $belang is not set');
96: }
97: // Needed - otherwise this won't work
98: $belang = false;
99: }
100:
101: self::init($cfg['path']['contenido_locale'], $belang, $domain);
102: }
103:
104: // Is emulator to use?
105: if (!$cfg['native_i18n']) {
106: $ret = self::emulateGettext($string, $domain);
107: // hopefully a proper replacement for
108: // mb_convert_encoding($string, 'HTML-ENTITIES', 'utf-8');
109: // see http://stackoverflow.com/q/11974008
110: $ret = htmlspecialchars_decode(utf8_decode(conHtmlentities($ret, ENT_COMPAT, 'utf-8', false)));
111: return $ret;
112: }
113:
114: // Try to use native gettext implementation
115: if (extension_loaded('gettext')) {
116: if (function_exists('dgettext')) {
117: if ($domain != 'contenido') {
118: $translation = dgettext($domain, $string);
119: return $translation;
120: } else {
121: return gettext($string);
122: }
123: }
124: }
125:
126: // Emulator as fallback
127: $ret = self::emulateGettext($string, $domain);
128: if (isUtf8($ret)) {
129: $ret = utf8_decode($ret);
130: }
131: return $ret;
132: }
133:
134: /**
135: * Returns the current language (if already defined)
136: *
137: * @return string|false
138: */
139: public static function getLanguage() {
140: return (self::$_i18nData['language']) ? self::$_i18nData['language'] : false;
141: }
142:
143: /**
144: * Returns list of registered domains
145: *
146: * @return array
147: */
148: public static function getDomains() {
149: return self::$_i18nData['domains'];
150: }
151:
152: /**
153: * Returns list of cached tranlation files
154: *
155: * @return array
156: */
157: public static function getFiles() {
158: return self::$_i18nData['files'];
159: }
160:
161: /**
162: * Returns list of cached tranlations
163: *
164: * @return array
165: */
166: public static function getCache() {
167: return self::$_i18nData['cache'];
168: }
169:
170: /**
171: * Resets cached translation data (language, domains, files, and cache)
172: */
173: public static function reset() {
174: self::$_i18nData['language'] = NULL;
175: self::$_i18nData['domains'] = array();
176: self::$_i18nData['files'] = array();
177: self::$_i18nData['cache'] = array();
178: }
179:
180: /**
181: * Emulates GNU gettext
182: *
183: * @param string $string The string to translate
184: * @param string $domain The domain to look up
185: * @return string Returns the translation
186: */
187: public static function emulateGettext($string, $domain = 'contenido') {
188: if ($string == '') {
189: return '';
190: }
191:
192: if (!isset(self::$_i18nData['cache'][$domain])) {
193: self::$_i18nData['cache'][$domain] = array();
194: }
195: if (isset(self::$_i18nData['cache'][$domain][$string])) {
196: return self::$_i18nData['cache'][$domain][$string];
197: }
198:
199: $translationFile = self::$_i18nData['domains'][$domain] . self::$_i18nData['language'] . '/LC_MESSAGES/' . $domain . '.po';
200:
201: if (!cFileHandler::exists($translationFile)) {
202: return $string;
203: }
204:
205: if (!isset(self::$_i18nData['files'][$domain])) {
206: self::$_i18nData['files'][$domain] = self::_loadTranslationFile($translationFile);
207: }
208:
209: $stringStart = strpos(self::$_i18nData['files'][$domain], '"' . str_replace(array(
210: "\n",
211: "\r",
212: "\t"
213: ), array(
214: '\n',
215: '\r',
216: '\t'
217: ), $string) . '"');
218: if ($stringStart === false) {
219: return $string;
220: }
221:
222: $matches = array();
223: $quotedString = preg_quote(str_replace(array(
224: "\n",
225: "\r",
226: "\t"
227: ), array(
228: '\n',
229: '\r',
230: '\t'
231: ), $string), '/');
232: $result = preg_match("/msgid.*\"(" . $quotedString . ")\"(?:\s*)?\nmsgstr(?:\s*)\"(.*)\"/", self::$_i18nData['files'][$domain], $matches);
233: // Old:
234: // preg_match("/msgid.*\"".preg_quote($string,"/")."\".*\nmsgstr(\s*)\"(.*)\"/",
235: // self::$_i18nData['files'][$domain], $matches);
236:
237: if ($result && !empty($matches[2])) {
238: // Translation found, cache it
239: self::$_i18nData['cache'][$domain][$string] = stripslashes(str_replace(array(
240: '\n',
241: '\r',
242: '\t'
243: ), array(
244: "\n",
245: "\r",
246: "\t"
247: ), $matches[2]));
248: } else {
249: // Translation not found, cache original string
250: self::$_i18nData['cache'][$domain][$string] = $string;
251: }
252:
253: return self::$_i18nData['cache'][$domain][$string];
254: }
255:
256: /**
257: * Registers a new i18n domain.
258: *
259: * @param string $localePath Path to the locales
260: * @param string $domain Domain to bind to
261: */
262: public static function registerDomain($domain, $localePath) {
263: if (function_exists('bindtextdomain')) {
264: // Bind the domain 'contenido' to our locale path
265: bindtextdomain($domain, $localePath);
266: }
267: self::$_i18nData['domains'][$domain] = $localePath;
268: }
269:
270: /**
271: * Loads gettext translation and file does some operations like stripping
272: * comments on the content.
273: *
274: * @param string $translationFile
275: * @return string The preparend translation file content
276: */
277: protected static function _loadTranslationFile($translationFile) {
278: $content = cFileHandler::read($translationFile);
279:
280: // Normalize eol chars
281: $content = str_replace("\n\r", "\n", $content);
282: $content = str_replace("\r\n", "\n", $content);
283:
284: // Remove comment lines
285: $content = preg_replace('/^#.+\n/m', '', $content);
286:
287: // Prepare for special po edit format
288: /*
289: * Something like: #, php-format msgid "" "Hello %s,\n" "\n" "you've got
290: * a new reminder for the client '%s' at\n" "%s:\n" "\n" "%s" msgstr ""
291: * "Hallo %s,\n" "\n" "du hast eine Wiedervorlage erhalten für den
292: * Mandanten '%s' at\n" "%s:\n" "\n" "%s" has to be converted to: msgid
293: * "Hello %s,\n\nyou've got a new reminder for the client '%s'
294: * at\n%s:\n\n%s" msgstr "Hallo %s,\n\ndu hast eine Wiedervorlage
295: * erhalten für den Mandanten '%s' at\n%s:\n\n%s"
296: */
297: // assemble broken long message lines (remove double quotes with a line
298: // break in between, e. g. "\n")
299: $content = preg_replace('/(""\\s+")/m', '"', $content);
300: // replace line breaks followed by a whitespace character against a line
301: // break
302: $content = preg_replace('/\\n"\\s+"/m', '\\n', $content);
303: // remove multiple line breaks
304: $content = preg_replace('/("\n+")/m', '', $content);
305: // remove the backslash from double quotes (\"foobar\" -> "foobar")
306: $content = preg_replace('/(\\\")/m', '"', $content);
307:
308: return $content;
309: }
310: }